1. 整体架构

ArrayList 整体架构比较简单,就是一个 Object 数组,如图所示。
ArrayList 解析 - 图1

有几个点需要注意:

  • DEFAULT_CAPACITY 表示数组的初始大小,默认是 10,这个数字要记住;
  • size 表示当前数组的大小,类型 int,没有使用 volatile 修饰,非线程安全的;
  • modCount 统计当前数组被修改的版本次数,数组结构有变动,就会 +1。

    2. 源码解析

    2.1 初始化

    ArrayList 有三个进行初始化的构造函数: ```java private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 无参构造器,返回一个 Object[] 空数组 // 无参构造器初始化时,默认大小是空数组,并不是大家常说的 10,10 是在第一次 add 的时候扩容的数组值。 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }

// 有参构造器,返回一个给定初始化大小的 Object[] 数组 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); } }

// 指定初始数据初始化 public ArrayList(Collection<? extends E> c) { // elementData 是保存数组的容器,默认为 null elementData = c.toArray(); // 如果给定的集合(c)数据有值,则进行拷贝赋值操作 if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) // 如果集合元素类型不是 Object 类型,才开始拷贝,否则不执行 if (elementData.getClass() != Object[].class) { elementData = Arrays.copyOf(elementData, size, Object[].class); } } else { // 给定集合(c)无值,则默认空数组 this.elementData = EMPTY_ELEMENTDATA; } }

  1. <a name="rOJh0"></a>
  2. # 2.2 新增和扩容
  3. **ArrayList 的 add() 方法分为两步:**
  4. 1. 判断是否需要扩容,如果需要先进行扩容
  5. 1. 直接赋值(不加锁,因此是线程不安全的)
  6. **以下是这两个步骤的相关源码:**
  7. ```java
  8. public boolean add(E e) {
  9. // 判断是否需要扩容,如果需要先进行扩容
  10. ensureCapacityInternal(size + 1); // Increments modCount!!
  11. // 直接赋值(不加锁,因此是线程不安全的)
  12. elementData[size++] = e;
  13. return true;
  14. }
  15. private void ensureCapacityInternal(int minCapacity) {
  16. // 如果是空数组,就从最小容量和默认容量10之间取最大值
  17. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  18. minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
  19. }
  20. // 确保容积足够,若容积不够就扩容
  21. ensureExplicitCapacity(minCapacity);
  22. }
  23. private void ensureExplicitCapacity(int minCapacity) {
  24. // 统计当前数组被修改的版本次数,数组结构有变动,就会 +1
  25. modCount++;
  26. // 如果我们希望的最小容量大于目前数组的长度,那么就扩容
  27. // 也就是 size+1 > elementData.length 的时候进行扩容,数组即将满的时候进行扩容
  28. if (minCapacity - elementData.length > 0)
  29. grow(minCapacity);
  30. }
  31. // 将原数组扩大1.5倍,把现有数据拷贝到新的数组里面去
  32. private void grow(int minCapacity) {
  33. // overflow-conscious code
  34. int oldCapacity = elementData.length;
  35. int newCapacity = oldCapacity + (oldCapacity >> 1);
  36. // 如果扩容后的值 < 我们的期望值,扩容后的值就等于我们的期望值
  37. if (newCapacity - minCapacity < 0)
  38. newCapacity = minCapacity;
  39. // 如果扩容后数组的大小 > jvm所能分配的数组的最大值,那么扩容后数组的大小就用 Integer 的最大值
  40. if (newCapacity - MAX_ARRAY_SIZE > 0)
  41. newCapacity = hugeCapacity(minCapacity);
  42. // minCapacity is usually close to size, so this is a win:
  43. // 通过复制进行扩容
  44. elementData = Arrays.copyOf(elementData, newCapacity);
  45. }

这里有几个需要注意的点:

  • 扩容的规则是将原数组扩大1.5倍,然后把现有数据拷贝到新的数组里面去。
  • ArrayList 中的数组的最大值是 Integer.MAX_VALUE,超过这个值,JVM 就不会给数组分配内存空间了。
  • 新增时,并没有对值进行严格的校验,所以 ArrayList 是允许 null 值的。
  • 扩容完成之后,赋值是非常简单的,直接往数组上添加元素即可:elementData[size++] = e。也正是通过这种简单赋值,没有任何锁控制,所以这里的操作是线程不安全的。

扩容的本质:
上面代码的最后一行是扩容操作:elementData = Arrays.copyOf(elementData, newCapacity);,这行代码描述的本质是数组之间的拷贝,扩容是会先新建一个符合我们预期容量的新数组,然后把老数组的数据拷贝过去,最后调用的是 System.arraycopy 方法进行拷贝,这是一个 native 方法。

2.3 删除

ArrayList 删除元素有很多种方式,比如根据数组索引删除、根据值删除或批量删除等等,原理和思路都差不多,这里以根据值删除为例:

  1. // 根据值删除
  2. public boolean remove(Object o) {
  3. // 如果要删除的值是 null,找到第一个值是 null 的删除
  4. if (o == null) {
  5. for (int index = 0; index < size; index++)
  6. if (elementData[index] == null) {
  7. fastRemove(index);
  8. return true;
  9. }
  10. } else {
  11. // 值不为空,找到第一个和入参相等的删除
  12. for (int index = 0; index < size; index++)
  13. // 这里是根据 equals 来判断值相等的
  14. if (o.equals(elementData[index])) {
  15. fastRemove(index);
  16. return true;
  17. }
  18. }
  19. return false;
  20. }

需要注意

  1. 新增的时候是没有对 null 进行校验的,所以可以删除 null 值
  2. 算法实现是通过遍历数组找到要删除的目标,然后进行删除

上面代码已经找到要删除元素的索引了,下面代码是根据索引位置进行元素的删除:

private void fastRemove(int index) {
    // 统计当前数组被修改的版本次数,数组结构有变动,就会 +1
    modCount++;
    // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0 开始算起
    // numMoved 删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 从 index +1 位置开始被拷贝,拷贝的起始位置是 index
        System.arraycopy(elementData, index + 1, elementData, index, numMoved);
    // 数组最后一个位置赋值为 null,帮助 GC
    elementData[--size] = null; // clear to let GC do its work
}

从源码中,我们可以看出,某一个元素被删除后,为了维护数组结构,我们都会把数组后面的元素往前移动。

2.4 迭代器

如果要实现迭代器,那么实现 java.util.Iterator 类就好了,ArrayList 也是这样做的。

// 实现 Iterator 接口
private class Itr implements Iterator<E> {
    // 迭代过程中,下一个元素的位置,默认从 0 开始。
    int cursor;       // index of next element to return
    // 新增时表示上一次迭代过程中,索引的位置;删除成功时为 -1
    int lastRet = -1; // index of last element returned; -1 if no such
    // 迭代过程中期望数组修改版本号
    int expectedModCount = modCount;

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

    // 检验能不能继续迭代,并且找到迭代的值,并为下一次迭代做准备(cursor+1)。
    @SuppressWarnings("unchecked")
    public E next() {
        //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
        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];
    }

    public void remove() {
        // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
        if (lastRet < 0)
            throw new IllegalStateException();
        //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            // -1 表示元素已经被删除,这里也防止重复删除
            lastRet = -1;
            // 删除元素时 modCount 的值已经发生变化,再此赋值给 expectedModCount
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
    ...
}

2.5 线程安全

需要强调的是,只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。

为什么 ArrayList 会有线程安全问题?本质原因是 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

类注释中推荐我们使用 SynchronizedList 来保证线程安全,SynchronizedList 是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。JDK 还提供了另外一种线程安全的 List,叫做CopyOnWriteArrayList。

3. Arrays.asList使用指南

public static <T> List<T> asList(T... a) {
        return new ArrayList<>(a);
}

上面是Arrays.List的方法签名,参数类型是T,T是数组元素的class。asList方法的参数必须是对象或者对象数组,而原生数据类型不是对象——这也正是包装类出现的一个主要原因。如果传入一个原生数据类型数组,asList 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时List 的唯一元素就是这个数组。并且,方法返回的ArrayList并不是 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类。

因此,对于Arrays.asList的使用,新手很容易犯两个错误:

  1. 将原生数据类型数据的数组作为参数。
    1. 解决方案:使用包装类数组
  2. 试图修改 List 的大小。
    1. 解决方案:创建一个真正的 ArrayList