Loading

源码阅读之 ArrayList 类

源码阅读之 ArrayList 类

1 基本结构

以下是 ArrayList 类基础部分的简单结构,API 相关的未列出:

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    // 序列化用
    private static final long serialVersionUID = 8683452581122892189L;
    
    // 默认初始容量为 10
    private static final int DEFAULT_CAPACITY = 10;
    
    // 简称 EE,在 有参 构造 ArrayList 的时候用来表示空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    // 简称 DEE,在 无参 构造 ArrayList 的时候用来表示空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // elementData 存储实际的数据、size 表示数组中元素的数量
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;
    
    // 三个构造器
    public ArrayList(int initialCapacity);
    public ArrayList();
    public ArrayList(Collection<? extends E> c);
    
    // 三个内部类
    private class Itr implements Iterator<E> {}
    private class ListItr extends Itr implements ListIterator<E> {}
    private class SubList extends AbstractList<E> implements RandomAccess {}
    
    
    // 记录数据被结构性修改的次数
    protected transient int modCount = 0;
    
    // ArrayList 最大大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
}
    

2 初始化

详情见 EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 章节,其中涉及到的一个思想就是——懒(lazy),尤其是在创建空的 ArrayList 的时候。

  • 如果创建的 ArrayList 一开始的时候是空的,那么它的 elementData 一定引用一个(两个)共同的空数组,直到向其中添加元素它才会初始化,并且 elementData 的长度一定大于等于 10。
  • 如果通过已有的集合来创建 ArrayList,那么其 elementData 的长度等同于已有集合的长度。

2.1 从指定容器创建

这是一个比较坑的构造,不太符合我们的思维习惯:

public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        // 从已有集合构建时
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                // 如果参数也是 ArrayList,直接引用原数组!!!
                elementData = a;
            } else {
                // 如果对象不是 ArrayList,则使用 copyOf 方法,是浅拷贝!!!
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

从旧的容器构建新的容器,新容器里面的元素仍然会引用旧的元素

public static void testCopy() {
    class User {
        String name = "Hello";
    }
    User user = new User();
    ArrayList<User> list1 = new ArrayList<>();
    list1.add(user);
    ArrayList<User> list2 = new ArrayList<>(list1);
    System.out.println(list2.get(0).name);	// Hello
    list1.get(0).name = "hi";
    System.out.println(list2.get(0).name);	// hi
}

3 自动扩容

由于用数组 elementData 存储数据,ArrayList 需要在每次添加元素的时候都确认以下数组是否足够大,如果数组长度不足以存储下新的数据,则需要增加数组长度。

3.1 初始状态

以下讨论中假定 ArrayList 的初始状态是空(也就是说其中的 elementData 引用一个空数组)。

3.2 add(E e) 扩容

以下是 add 方法的调用链,由于大都是 ArrayList 内部的调用,所以不太明显,但是调用的顺序可以清楚地看到:

ArrayList_add

1 调用 add 方法直接添加元素:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

1.1 add 方法调用的第一个方法就是 ensureCapacityInternal,传入的实参是当前的数组中元素的数量 + 1,而虚参名称为 minCapacity,指意明确:

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

1.1.1 calculateCapacity 方法主要用来计算合理的最小容量:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果当前 ArrayList 为空,则取 DEFAULT_CAPACITY(10) 和 minCapacity(这里是 1)的最大值
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

从这里可以看出来,一旦 ArrayList 中实际存储了元素,那么它 elementData 的长度一定会大于等于 10

1.1.2 ensureExplicitCapacity 方法主要用来记录 modCount 的增加(扩容——更准确说是添加元素,因为只有添加了元素才会扩容,是一种结构性的修改,所以 modCount 需要增加)。

// overflow-conscious code 注释下的代码即是扩容的入口了,

private void ensureExplicitCapacity(int minCapacity) {
    // 这里的 minCapacity 已经是 >= 10 的一个值了
    modCount++;

    // overflow-conscious code	表示考虑了溢出问题
    if (minCapacity - elementData.length > 0)	// 如果当前需要的最小容量大于当前 elementData 的长度,那么则需要扩容,参数为 minCapacity
        grow(minCapacity);
}

1.1.2.1 grow 方法就像它的名字一样,成长、变大:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);	// 新的容量为 oldCapacity / 2 + oldCapacity = (3/2)* oldCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;	// 新的容量足够了
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);	// 新的容量比默认最大 ArrayList 长度还大
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

1.1.2.1.1 可能的 hugeCapacity 调用,这个方法主要用于处理溢出(前面用减法比较的 newCapacityMAX_ARRAY_SIZE 大小,可能会溢出)

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();	// 如果在初始化 ArrayList 的时候指定一个 MAX_ARRAY_SIZE 的
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;
}

1.1.2.1.2 Arrays.copyOf 方法

Arrays.copy 调用 System.arraycopy,后者则调用了本地方法。

前者会自动创建并且返回新的数组,后者则是需要指定复制后的接受数组对象。扩容时一定需要创建新的数组,所以使用 Arrays.copy 方便(虽然它最终还是回去调用 System.arraycopy ),但是在已经扩容后插入元素的操作中进行批量元素复制的话就不要使用 Arrays.copy 方法了,因为这样又会创建一个新的数组

3.3 add(index, e) 扩容

以下是源码,可以看到其无非就是多了一个 index 合法性的判断和插入位置之后的元素移动:

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

3.4 addAll() 扩容

没啥好说的了,原理基本一致。但是它不用一次次地扩容,而是一次性扩让到指定大小。

public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    return numNew != 0;
}

public boolean addAll(int index, Collection<? extends E> c) {
    rangeCheckForAdd(index);

    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);

    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;
}

4 手动扩容

开发者为我们留下了这样的一个公有方法,但是自身并未使用:

// Increases the capacity of this ArrayList instance, if necessary, to ensure that it can hold at least the number of elements specified by the minimum capacity argument.
// Params:
// minCapacity – the desired minimum capacity
public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

通过观察自动扩容机制,我们可以看到,如果写出类似如下的代码:

for (int i = 0; i < 1000; i++) {
    list.add(i);
}

调用 ensureCapacity 它,并且指定实参为 1000,免去自动扩容过程中重复地创建新的数组(Arrays.copyOf 方法扩容)

5 remove

remove()方法也有两个版本,一个是remove(int index)删除指定位置的元素,另一个是remove(Object o)删除第一个满足o.equals(elementData[index])的元素。删除操作是add()操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null值。

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    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

    return oldValue;
}

对象能否被GC的依据是是否还有引用指向它,上面代码中如果不手动赋 null 值,除非对应的位置被其他元素覆盖,否则原来的对象就一直不会被回收。

6 EE 和 DEE

EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA,最开始看到这两个静态变量是感到非常疑惑的,虽然说看在构造空 ArrayList 的时候直接把这两个变量赋值给新的 ArrayList 对象中的 elementData 很方便,但是感觉大可不必,直接创建一个新的空数组然后赋值给 elementData 不好吗?

以下是 java7ArrayList 的构造器代码(时间问题,没来得及考证。。。):

public ArrayList(int initialCapacity) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
}

public ArrayList() {
    super();
    this.elementData = EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    size = elementData.length;
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elementData.getClass() != Object[].class)
        elementData = Arrays.copyOf(elementData, size, Object[].class);
}

以下是 java8 中 ArrayList 的构造器代码:

public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 在这里增加了容量为 0 的判断
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

public ArrayList() {
    // 创建的时候是一个空数组,长度不是默认为 10
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array. 这是官方的注释
        elementData = EMPTY_ELEMENTDATA;
    }
}

首先,为什么创建一个空的(无参构造) ArrayList 不直接使用 elementData = new Object[initialCapacity] 呢?

因为创建一个对象需要消耗大量的资源!这点很容易理解了,除非这个 ArrayList 中的 elementData 真的要存储东西,不然我就让它先引用所有 ArrayList 全局共享的一个空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),当真的需要 elementData 存储数据的时候,我再给它创建单独的对象(add 系方法中提现),这应该也是一种懒加载(lazy)的体现。

7 迭代与 fail-fast

众所周知,使用迭代器迭代过程中是不允许对容器进行结构性修改(增加、减少元素)的(使用迭代器的 remove 方法除外),modCount 字段就是专门来应对这个问题的。

以下是 ArrayList.Iterator 的基本结构:

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;

    Itr() {}

    public boolean hasNext()
    public E next()
    public void remove()
    public void forEachRemaining(Consumer<? super E> consumer)
    final void checkForComodification()
}

ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,这点具体体现在迭代器的 next 方法中:

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();
}

而不是冒着在将来某个不确定时间发生任意不确定行为的风险。

参看这篇文章:https://www.javatpoint.com/fail-fast-and-fail-safe-iterator-in-java

还有下面一小段 demo:

public static void testConcurrentModify() {
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
    Iterator<Integer> iterator = list.iterator();
    int count = 0;
    while (iterator.hasNext()) {
        Integer next = iterator.next();
        if (count == 0) {
            list.remove(0);
            count = 1;
        }
        System.out.println(next);
        // 输出 1 后抛出异常
    }
}

public static void testConcurrentModifyForEachRemaining() {
    ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
    Iterator<Integer> iterator = list.iterator();
    int count = 0;
    iterator.forEachRemaining(e -> {
        System.out.println(e);
        list.remove(0);
    });
    // 输出 1 后抛出异常
}
posted @ 2022-04-28 00:03  槐下  阅读(32)  评论(0编辑  收藏  举报