瞅瞅Java基础源码(一)——ArrayList

JDK版本: 1.8
IDEA版本:2020.01

正片开始


一、继承关系

在这里插入图片描述
顶层父类是Iterable类,

二、源码解读

2.1 构造函数

2.1.1 无参构造函数:ArrayList()

/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

DEFAULTCAPACITY_EMPTY_ELEMENTDATA其实就是一个空数组私有常量

/**
* Shared empty array instance used for default sized empty instances. We
* distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
* first element is added.
*/
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

然后就没了。可以看到ArrayList的无参构造函数都没有去管elementData的长度需要多少直接设置为一个空数组(也就是长度为0的数组),所以说在调用无参构造函数初始化一个ArrayList的时候

ArrayList<String> list = new ArrayList();

初始化数组长度为0,不要听风是雨觉得ArrayList的初始化长度是10,应该要指出在无参构造后第一次进行add操作时才初始化数组长度为10。

2.1.2 ArrayList(int initialCapacity)

/**
* Constructs an empty list with the specified initial capacity.
*
* @param  initialCapacity  the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
*         is negative
*/
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}

不多说这里的逻辑了,就是很疑惑这里为啥又来一个EMPTY_ELEMENTDATA,我看它的值跟DEFAULTCAPACITY_EMPTY_ELEMENTDATA是一样一样的。看了下DEFAULTCAPACITY_EMPTY_ELEMENTDATA的注释,大概意思是在第一次add(E e)时用来区分是否要把容量初始化为默认的10(用的是这个私有常量:DEFAULT_CAPACITY)。

/**
* Shared empty array instance used for empty instances.
*/
private static final Object[] EMPTY_ELEMENTDATA = {};

2.1.3 ArrayList(Collection<? extends E> c)

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();把传进来的集合转化为数组并赋值给了elementData
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            这里的if判断我也有点疑惑,前面的源码注释好像告诉我没加这个if前这可能会引发bug
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;设置为{}
    }
}

2.2 常用方法

2.2.1 public boolean add(E e)

public boolean add(E e) {
    确保容量够,不够则扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!这里用注释提醒在ensureCapacityInternal方法里modCount会增加,modCount是用来判断是否fast-fail
    elementData[size++] = e;赋值并size+1
    return true;
}
保证容量的方法(内部使用)
private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        如果是默认的空数组则取minCapacity和默认值10的最大值
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);“我可能要开始扩容了”
}
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;终于见到modCount了,这玩意是给fast-fail机制用的

    // overflow-conscious code哦吼,具有溢出意识的code
    if (minCapacity - elementData.length > 0)大于数组容量,就扩容呗
        grow(minCapacity);
}
真正进行扩容的地方
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;保存旧的容量
    int newCapacity = oldCapacity + (oldCapacity >> 1);新容量等于1.5倍旧容量(或者等于1.5倍旧容量再减一,因为奇数除以2会丢失1)
    if (newCapacity - minCapacity < 0)计算出新容量是不是还小于需要的容量
        newCapacity = minCapacity;是的话木有办法了只能用需要的容量了
    if (newCapacity - MAX_ARRAY_SIZE > 0)新容量已经大于MAX_ARRAY_SIZE=Integer.MAX_VALUE - 8
        newCapacity = hugeCapacity(minCapacity);计算大容量,结果有三种,聊到再说
    // minCapacity is usually close to size, so this is a win:哈哈,源码作者觉得代码大部分情况下minCapacity应该是要跟size接近的
    elementData = Arrays.copyOf(elementData, newCapacity);根据新容量拷贝一个数组并赋值给elementData
}

2.2.2 public void add(int index, E element)

public void add(int index, E element) {
    rangeCheckForAdd(index);检查是否小于0或者大于size,说明index可以等于size(这时候其实就跟直接add(E e)差不多了)
    ensureCapacityInternal(size + 1);  // Increments modCount!!确保容量够,这个方法上面的add(E e)有出现过,这里不再说明
    System.arraycopy(elementData, index, elementData, index + 1, size - index);index以及后面的元素全部往后移一位
    elementData[index] = element;将添加的元素放置在index的位置
    size++;size+1
}

这里插播一条杂谈:

ArrayList使用数组实现,数组容量实际上不能真正的动态扩展,因为数组本质上是一连串连续的地址,而我们感受到ArrayList的扩容能力其实是开辟了一个更大容量的数组,并把旧数组的内容依次赋值到新数组上,最后把原来的数组引用(指针)指向新数组。所以很多时候都会用到Arrays.copyOf(T[] original, int newLength)Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)java System.arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
(其实无论是Arrays.copyOf(T[] original, int newLength)Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)中最终也是用到了System.arraycopy这个native方法)

2.2.3 public E get(int index)

好嘛~就两行,不过想想也确实,毕竟是数组,根据index进行定位那是O(1)的时间/空间复杂度啊
public E get(int index) {
    rangeCheck(index);范围检查

    return elementData(index);
}
private void rangeCheck(int index) {
    if (index >= size)大于size的抛IndexOutOfBoundsException异常,小于0的这里没有去管
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
    return (E) elementData[index];取出对应位置后的元素根据泛型强转,这里index<0也会抛IndexOutOfBoundsException异常
}

2.2.4 public int size(),public boolean isEmpty(),public void clear()

这三个的源码比较简单直观,一起吧

public int size() {
    return size;二话不说,直接返回即可
}
public boolean isEmpty() {
    return size == 0;size为0则为empty
}
public void clear() {
    modCount++;修改数组元素的操作都会有这玩意
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;全部置为空,让原来的元素不被引用,等待这些元素的是GC
    size = 0;重置size为0
}

2.2.5 public int indexOf(Object o)

这个方法是找出第一个与o相同元素的index。

public int indexOf(Object o) {
    if (o == null) {
    	如果o为null遍历时直接用==null判断元素
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
    	如果o不为null就用equals了
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;找不到就返回-1
}

2.2.6 public boolean contains(Object o)

public boolean contains(Object o) {
	直接复用indexOf查找的结果,如果查找的结果大于等于0那就是包含该元素咯
    return indexOf(o) >= 0;
}

有的疑惑为啥要根据o是否为null在用代码中用了两个for循环,这不是很冗杂吗,直接这样它不香吗?

public int indexOf(Object o) {
    for (int i = 0; i < size; i++)
        if (o == null ? elementData[i]==null : o.equals(elementData[i]))
            return i;
    return -1;
}

我还就真的测了一下,发现在千万级别的差别真心不大,更别说大多数场景下的使用了。

2.2.7 public int lastIndexOf(Object o)

这个跟indexOf差不多,只不过换成了反向查找

public int lastIndexOf(Object o) {
    if (o == null) {
        for (int i = size-1; i >= 0; i--)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = size-1; i >= 0; i--)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

2.2.8 public E set(int index, E element)

替换index位置的元素

public E set(int index, E element) {
    rangeCheck(index);范围检查
    E oldValue = elementData(index);保存旧值
    elementData[index] = element;替换成新值
    return oldValue;返回旧值
}

2.2.9 public E remove(int index)

去除index位置的元素

public E remove(int index) {
    rangeCheck(index);范围检查
    modCount++;
    E oldValue = elementData(index);保存旧值
    把index位置后的元素往前移一位,这里在计算要移动的元素个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
    	/*
    	前移一位但其实是index位置后的位置往前复制了,复制后最后的元素会在结尾连续出现两份,
    	所以下一步将最后一个元素置null。
    	举个例子:假设elementData = [0, 1, 2],要remove的index为1,那么经过这一步后,
    	elementData = [0, 2, 2]
    	*/
        System.arraycopy(elementData, index+1, elementData, index, numMoved);
    elementData[--size] = null; // clear to let GC do its work经过这一步后,elementData = [0, 2, 2]将变成elementData = [0, 2, null]
    return oldValue;返回旧值
}

2.2.10 public boolean remove(Object o)

for循环查找,然后remove掉

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;

fastRemove的逻辑跟public E remove(int index)里的差不多。。

private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

2.2.11 public boolean addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();先把传来的集合转化为Object数组
    int numNew = a.length;确定新增的个数
    ensureCapacityInternal(size + numNew);  // Increments modCount确保容量够
    System.arraycopy(a, 0, elementData, size, numNew);从数组a的0位置开始复制numNew个元素到elementData(从size位置开始)
    size += numNew;调整size
    return numNew != 0;
}

三、其他

3.1 关于for循环中remove

ArrayList<Integer> list = new ArrayList<>(Arrays.asList(0, 1, 2, 3));
System.out.println("before removing, list = " + list);//①
for (int i = 0; i < list.size(); i++) {
  list.remove(i);
}
System.out.println("after removing,  list = " + list);//②

第一眼看去似乎①应该输出before removing, list = [0, 1, 2, 3],且②应该输出after removing, list = []
然而②的结果是after removing, list = [1, 3],导致这个结果的原因是i在不停增长,而size在不停减小(元素elementData中不断左移),过程大致是是这样的:
i = 0,list.size() = 3;list = [0, 1, 2, 3]
经过remove(i)即remove(0)后,list = [1, 2, 3],i++ ——> i = 1

i = 1,list.size() = 2;list = [1, 2, 3]
经过remove(i)即remove(1)后,list = [1, 3],i++ ——> i = 2

然后发现i = 2 已经 不满足 i < list.size()=2的条件了,便结束循环,所以结果就是list = [1, 3]。

所以当你写到类似下面的这段逻辑代码时:

for (int i = 0; i < list.size(); i++) {
  Integer a = list.get(i);
  if (a > 1) {//match condition then remove the element
    remove(i);
  }
}

只要对list进行反向遍历,就没有上面正向遍历的问题了:

for (int i = list.size() - 1; i >= 0; i--) {
  Integer a = list.get(i);
  if (a > 1) {//match condition then remove the element
    remove(i);
  }
}

3.2 关于fast-fail

单线程情况下关于骚操作导致的ConcurrentModificationException,如下:

import java.util.ArrayList;
import java.util.Arrays;

/**
 * @author NOknow
 * @version 1.0
 * @date 2020/09/05
 */
public class FastFail {

  public static void main(String[] args) {
    ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
    for (String s : list) {
      list.remove(s);
      System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
    }
  }
}

控制台结果:

'a' has been removed, current list is:[b, c, d]
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
	at java.util.ArrayList$Itr.next(ArrayList.java:851)
	at com.jw.collection.list.arraylist.FastFail.main(FastFail.java:16)

what?为啥只有元素a被移除了?为啥报错的是第16行?
在这里插入图片描述

要回答这两个问题就要去看对应的class文件了。
地球人都知道编译器把foreach循环替换成了迭代器了,class文件样貌如下:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;

public class FastFail {
  public FastFail() {
  }

  public static void main(String[] args) {
    ArrayList<String> list = new ArrayList(Arrays.asList("a", "b", "c", "d"));
    Iterator var2 = list.iterator();

    while(var2.hasNext()) {
      String s = (String)var2.next();
      list.remove(s);
      System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
    }

  }
}

于是再跟进list.iterator(),发现他返回了一个ArrayList的内部的一个迭代器:

public Iterator<E> iterator() {
    return new Itr();
}

Itr类内容(非全)如下:

private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;//这里是个重点

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();//这里也是个重点
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)//好了,恭喜你发现了最终奥秘
                throw new ConcurrentModificationException();
        }
        /*此处省略了一些其他源代码*/
    }

整个过程就是:
1、list.iterator();返回一个迭代器,迭代器中的expectedModCount的值来自list,这时候迭代器的expectedModCount与list的modCount相等,都等于0(不要问我为什么等于0)。
2、代码来到while(var2.hasNext()),很正常,进入while循环里。
3、代码来到String s = (String)var2.next();,很明显此时还是:迭代器的expectedModCount与list的modCount相等,都等于0。
4、代码来到list.remove(s);,关键点来了,从上面的源码分析可以知道,remove里对list的modCount进行了自增,此时:迭代器的expectedModCount=0,But !!!但是!!!, list的modCount=1。此时remove操作还是很正常地进行下去的,好,继续。
5、代码来到System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));,没毛病。
6、新的循环开始:代码来到while(var2.hasNext()),没毛病,继续。
7、代码来到String s = (String)var2.next();,好了,报错了,就是因为迭代器的next()函数的第一行代码:checkForComodification();,此时:迭代器的expectedModCount=0 != list的modCount=1

是不是才反应过来:“哦,原来你用list的迭代器去遍历元素,然后又用list的remove操作去除元素,应该直接用迭代器本身去remove的呀。”——对咯,就是这样:

public static void main(String[] args) {
  ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
  Iterator<String> iterator = list.iterator();
  while (iterator.hasNext()) {
    String s = iterator.next();
    iterator.remove();
    System.out.println(String.format("'%s' has been removed, current list is:%s", s, list));
  }
}

控制台打印结果:

'a' has been removed, current list is:[b, c, d]
'b' has been removed, current list is:[c, d]
'c' has been removed, current list is:[d]
'd' has been removed, current list is:[]

快~蹭着热乎,去把迭代器的remove的原理搞清楚。(还是去把ArrayList的迭代器的实现都看看吧)

多线程情况下导致的fast-fail的原因本质跟上面是一样一样的,人家ArrayList初心就是给单线程用的,不然也不会导致Vector失宠。

jend.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值