JDK源码阅读—ArrayList

简介

ArrayList 是一个数组列表,相当于动态数组。与Java中的数组相比,它的容量能动态增长。
以下分析基于corretto-1.8.0_282版本。

继承关系

ArrayList继承关系

  1. 实现了RandomAccess接口,代表其可进行快速随机访问,因为底层使用数组储存元素,所以可直接通过下标访问元素。
  2. 实现了Cloneable接口,可以通过.clone()方法克隆出一个实例。
  3. 实现了Serializable接口,可以被序列化。
  4. 实现了List接口,提供了添加、删除、遍历等操作。

属性

DEFAULT_CAPACITY

/**
 * 默认的初始容量
 * 实例化ArrayList时,如果不指定初始大小,则以此值作为ArrayList的初始容量
 */
private static final int DEFAULT_CAPACITY = 10;

EMPTY_ELEMENTDATA

/**
 * 空实例使用的共享空数组
 * 若初始化时指定大小为0,即new ArrayList(0),则将elementData赋值为EMPTY_ELEMENTDATA
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

DEFAULTCAPACITY_EMPTY_ELEMENTDATA

/**
 * 以默认容量初始化的ArrayList,其elementData会初始
 * 化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
 * 待添加第一个元素时,会以DEFAULT_CAPACITY的大小开辟数组空间
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

elementData

/**
 * 存储ArrayList中元素的数组。
 * ArrayList的容量是此数组的长度。 
 * transient关键字说明不会对此字段进行序列化
 * ArrayList通过自定义writeObject()方法对其进行序列化,通过
 * readObject()方法对其进行反序列化
 */
transient Object[] elementData;

size

/**
 * ArrayList的大小(它包含的元素数)
 */
private int size;

MAX_ARRAY_SIZE

/**
 * 要分配的数组的最大大小,申请大于此大小的数组可能
 * 会抛出异常OutOfMemoryError: Requested array size exceeds VM limit
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造方法

ArrayList()

/**
 * 按默认容量实例化的构造函数
 * 此构造函数不会开辟数组空间,而是等到插入第一个元素时才真正开辟数组空间
 */
public ArrayList() {
    // 将元素数组赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    // 即实例化时先赋值为空数组,待插入第一个元素时才真正开辟数组空间
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

ArrayList(int initialCapacity)

/**
 * 按指定容量实例化的构造函数
 * 此构造函数会按传入的容量开辟数组空间
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 若指定容量大于0,则按指定容量初始化元素数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 若指定容量等于0,则将元素数组赋值为EMPTY_ELEMENTDATA
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        // 若指定容量小于0,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: "+
                                            initialCapacity);
    }
}

ArrayList(Collection<? extends E> c)

/**
 * 按给定集合实例化的构造函数
 */
public ArrayList(Collection<? extends E> c) {
    // 传入集合转为数组
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            // 若集合类型为ArrayList,则toArray()方法返回的
            // 一定是Object[]类型,所以直接赋值给elementData
            elementData = a;
        } else {
            // 否则,toArray()方法返回的不一定是Object[]类型,需要做类型转换
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // 若集合大小为0,则将元素数组赋值为EMPTY_ELEMENTDATA
        elementData = EMPTY_ELEMENTDATA;
    }
}

方法

add(E e) 方法

将元素添加至列表末尾。

public boolean add(E e) {
    // 确保元素数组有足够的空间,同时修改次数(modCount)+1
    ensureCapacityInternal(size + 1);
    // 列表末尾添加元素,同时列表大小+1
    elementData[size++] = e;
    return true;
}

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

/**
 * 计算所需要的大小
 * 如果是第一次添加元素,则取DEFAULT_CAPACITY和minCapacity之中的最大值
 * 否则返回minCapacity,这里是当前列表大小+1
 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/**
 * 判断elementData大小是否满足传入的容量,不满足则进行扩容
 */
private void ensureExplicitCapacity(int minCapacity) {
    // 修改次数+1,用于做快速失败(fail-fast)检查
    modCount++;

    // 若需要的容量大于elementData的大小,则对elementData进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/**
 * 对elementData进行扩容
 */
private void grow(int minCapacity) {
    // 保存旧容量
    int oldCapacity = elementData.length;

    // 新容量设置为旧容量的1.5倍(右移一位相当于除以2)
    int newCapacity = oldCapacity + (oldCapacity >> 1);

    // 若1.5倍还是小于需要的容量minCapacity,则将新容量设置为minCapacity
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    // 如果新容量超过定义的最大数组容量MAX_ARRAY_SIZE,则判断需要的容量是否也大于定义的最大数组容量
    // 若需要的容量也大于MAX_ARRAY_SIZE则将新容量设置为Integer.MAX_VALUE,否则将新容量设置为MAX_ARRAY_SIZE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    
    // 以新容量对elementData进行扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    // 如果minCapacity小于0,说明minCapacity超过Integer.MAX_VALUE发生溢出,抛出异常
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}
  1. 检查是否需要扩容,同时将修改次数(modCount)+1。
    1. 若为第一次添加元素,此时elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则将elementData初始化为默认大小DEFAULT_CAPACITY。
    2. 新容量为旧容量的1.5倍,若调整为1.5倍之后仍然小于所需容量,则按需要的容量为准。
    3. 如果新容量超过定义的最大数组容量MAX_ARRAY_SIZE,则判断需要的容量是否也大于定义的最大数组容量,若需要的容量也大于MAX_ARRAY_SIZE则将新容量设置为Integer.MAX_VALUE,否则将新容量设置为MAX_ARRAY_SIZE。
    4. 使用新的容量创建数组并把元素复制到新数组中。
  2. 在列表末尾添加元素,同时列表大小加一。

add(int index, E element) 方法

在指定索引处添加元素。

public void add(int index, E element) {
    // 检查索引是否合法
    rangeCheckForAdd(index);
    
    // 确保元素数组有足够空间
    ensureCapacityInternal(size + 1);

    // 从指定位置将所有元素向后移动一位
    System.arraycopy(elementData, index, elementData, index + 1,
                        size - index);

    // 将元素插入指定位置
    elementData[index] = element;

    // 列表大小+1
    size++;
}

private void rangeCheckForAdd(int index) {
    // 若指定下标小于0或大于当前列表大小,抛出异常
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
  1. 检查指定下标是否合法。
  2. 确保元素数组有足够的空间,同时修改次数(modCount)+1。
  3. 若指定下标不在列表末尾,则需要从此下标处将所有元素向后移动一位。
  4. 将元素插入指定位置。
  5. 元素个数+1。

addAll(Collection<? extends E> c) 方法

将给定集合中的所有元素添加到列表末尾。

public boolean addAll(Collection<? extends E> c) {
    // 集合转数组
    Object[] a = c.toArray();
    int numNew = a.length;

    // 确保元素数组有足够的空间,同时修改次数(modCount)+1
    ensureCapacityInternal(size + numNew);

    // 将集合的数组复制到elementData的末尾
    System.arraycopy(a, 0, elementData, size, numNew);

    // 修改元素个数
    size += numNew;
    return numNew != 0;
}
  1. 调用传入集合的toArray()方法将其转为数组。
  2. 确保元素数组有足够的空间,同时修改次数(modCount)+1。
  3. 将集合的数组复制到elementData的末尾。
  4. 修改元素个数。

addAll(int index, Collection<? extends E> c) 方法

将给定集合中的所有元素添加进列表指定索引处,即求两个集合的并集。

public boolean addAll(int index, Collection<? extends E> c) {
    // 确保索引在[0, size](闭区间)之间
    rangeCheckForAdd(index);

    // 集合转数组
    Object[] a = c.toArray();
    int numNew = a.length;

    // 确保元素数组有足够空间
    ensureCapacityInternal(size + numNew);

    // 获取需要移动元素的个数,若指定索引在列表末尾,则不需要进行移动
    int numMoved = size - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index, elementData, index + numNew,
                            numMoved);
    // 将集合数组复制到elementData的指定位置
    System.arraycopy(a, 0, elementData, index, numNew);

    // 修改元素个数
    size += numNew;
    return numNew != 0;
}
  1. 检查索引是否合法。
  2. 调用传入集合的toArray()方法将其转为数组,获取其元素个数numNew。
  3. 确保元素数组有足够的空间,同时修改次数(modCount)+1。
  4. 若索引不是列表末尾,则需要将此索引之后的所有元素向后移动numNew位。
  5. 将传入集合的元素数组复制到elementData的指定位置。
  6. 列表大小+numNew。

get(int index) 方法

获取指定索引处的元素。

public E get(int index) {

    // 检查索引是否越界
    rangeCheck(index);

    return elementData(index);
}

/**
 * 检查索引是否越界
 */
private void rangeCheck(int index) {
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/**
 * 返回指定索引处的元素,并按照泛型做强制类型转换
 */
E elementData(int index) {
    return (E) elementData[index];
}
  1. 检查索引是否越界,这里只检查了上界,下界交由JVM检查,具体原因不明这里有一篇帖子说明了为什么不检查下界 Why does ArrayList#rangeCheck not check if the index is negative?
  2. 返回指定索引处的元素,并根据泛型做强制类型转换。

set(int index, E element) 方法

修改指定索引处的元素。

public E set(int index, E element) {
    // 检查索引是否越界
    rangeCheck(index);

    // 保存旧元素,作为方法返回值
    E oldValue = elementData(index);

    // 更新元素
    elementData[index] = element;
    return oldValue;
}
  1. 检查索引是否越界。
  2. 保存旧元素,作为方法返回值。
  3. 更新指定索引处的元素。

remove(int index) 方法

移除指定索引处的元素。

public E remove(int index) {
    // 检查索引是否越界
    rangeCheck(index);

    // 修改次数+1
    modCount++;

    // 保存此位置的元素,作为方法返回值
    E oldValue = elementData(index);

    // 将指定索引后的所有元素向前移动一位
    // 计算需要移动的元素个数,若删除的是最后一个元素,则不需要移动
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);

    // 将末尾空出的位置置为null,帮助GC回收,同时列表大小减一
    elementData[--size] = null;

    return oldValue;
}
  1. 检查索引是否越界。
  2. 修改次数(modCount)+1。
  3. 保存旧元素,作为方法返回值。
  4. 若索引不是列表末尾,则需要将此索引之后的所有元素向前移动一位。
  5. 将列表末尾置为null,帮助GC回收内存,同时列表大小减一。

remove(Object o) 方法

移除指定元素。

public boolean remove(Object o) {
    // 对于null和非null元素做了区分,防止触发NPE
    // 从头开始遍历,移除第一个匹配的元素
    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;
}

private void fastRemove(int index) {
    // 修改次数+1
    modCount++;

    // 将指定位置后的所有元素向前移动一位
    // 计算需要移动的元素个数,若删除的是最后一个元素,则不需要移动
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
    // 将末尾空出的位置置为null,帮助GC回收,同时列表大小减一
    elementData[--size] = null;
}
  1. 判断传入元素是否为null,为null使用 == 进行比对,不为null使用equals比较。
  2. 从下标0开始遍历元素数组elementData,对第一个匹配的元素进行删除。
    1. 修改次数+1。
    2. 若被删除元素索引不在列表末尾,则需要将此索引之后的所有元素向前移动一位。
    3. 将末尾空出的位置置为null,帮助GC回收内存,同时列表大小减一。

removeAll(Collection<?> c) 方法

删除给定集合中存在的元素,即求两个集合的差集。

public boolean removeAll(Collection<?> c) {
    // 判空
    Objects.requireNonNull(c);
    return batchRemove(c, false);
}

/**
 * 批量删除
 * complement为true代表删除c中不存在的元素
 * complement为false代表删除c中存在的元素
 */
private boolean batchRemove(Collection<?> c, boolean complement) {
    final Object[] elementData = this.elementData;
    // r为读指针,遍历整个elementData
    // w为写指针,只有在满足条件写入时才+1
    int r = 0, w = 0;
    boolean modified = false;
    try {
        for (; r < size; r++)
            // 根据c中是否包含此元素以及complement参数的值决定是否保留此元素
            if (c.contains(elementData[r]) == complement)
                elementData[w++] = elementData[r];
    } finally {
        // 如果c.contains抛出异常,则r可能不等于size
        // 此时需要将elementData中所有还未读取的元素复制到写指针后
        if (r != size) {
            System.arraycopy(elementData, r,
                                elementData, w,
                                size - r);
            w += size - r;
        }
        
        if (w != size) {
            // 将写指针后的元素置为null,帮助GC回收
            for (int i = w; i < size; i++)
                elementData[i] = null;
                
            // 修改次数+(size - w) (size - w 为删除的元素个数)
            modCount += size - w;
            
            // 修改列表大小
            size = w;
            modified = true;
        }
    }
    return modified;
}
  1. 遍历整个elementData,将c中存在的元素向前移动,覆盖掉c中不存在的元素,遍历完成后,写指针w前的元素为两个集合都存在的元素。
  2. 将写指针w之后的元素置为null,帮助GC回收内存。
  3. 修改列表大小。

removeIf(Predicate<? super E> filter) 方法

根据条件移除元素。

public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);

    // 标记所有要被移除的元素
    // 在此阶段所有filter引发的异常将会使集合保持不变
    int removeCount = 0;
    // 标记需要删除元素的位图
    final BitSet removeSet = new BitSet(size);

    // 保存当前modCount值
    final int expectedModCount = modCount;
    final int size = this.size;
    
    // 开始标记阶段
    // 对所有元素进行遍历,同时会对modCount进行检查,若有其他线程对集合进行了修改,则抛出异常
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        // 调用filter进行判断
        if (filter.test(element)) {
            // 如果此元素需要删除,则将位图此索引处的位置为true
            removeSet.set(i);
            removeCount++;
        }
    }

    // 若在标记期间集合被修改,抛出异常
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }

    // 开始删除阶段
    final boolean anyToRemove = removeCount > 0;
    if (anyToRemove) {
        // 移除符合条件元素后的新大小
        final int newSize = size - removeCount;

        // 保留elementData中的所有非删除元素
        // 指针i遍历所有非删除元素的下标
        // 指针j每次递增
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
            // 取得位图中从索引i开始,第一个设置为false的位的索引,即取得
            // 从i开始,第一个非删除元素的索引
            i = removeSet.nextClearBit(i);
            elementData[j] = elementData[i];
        }

        // 将空出的位置置为null,帮助GC回收
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;
        }
        this.size = newSize;

        // 若此期间有其他线程修改集合,则抛出异常
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

    return anyToRemove;
}
  1. 标记阶段
    1. 对所有元素进行遍历,同时会对modCount进行检查,若有其他线程对集合进行了修改,则抛出异常。
    2. 如果此元素需要删除,则将位图此索引处的位设置为true。
  2. 删除阶段
    1. 计算删除后的新大小newSize。
    2. 根据标记位图将elementData中所有非删除元素向前移动,覆盖标记了删除的元素。
    3. 从newSize开始,将elementData后面的所有位置设置为null,帮助GC回收内存。
    4. 若此操作期间有其他线程修改集合,则抛出异常。
    5. 修改次数+1。

retainAll(Collection<?> c) 方法

保留指定集合中存在的元素,删除其他元素,即求两个集合的交集。

public boolean retainAll(Collection<?> c) {
    // 判空
    Objects.requireNonNull(c);
    // 批量删除,与removeAll(Collection<?> c)方法类似,只是这里是删除所有不在c中的元素
    return batchRemove(c, true);
}

trimToSize()方法

对列表进行缩容。

public void trimToSize() {
    // 修改次数+1
    modCount++;
    
    // 如果元素个数小于数组大小,对elementData进行缩容
    // 如果size为0,则将elementData置为EMPTY_ELEMENTDATA,
    // 否则将elementData的大小调整为size
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}
  1. 若列表大小为0,则将elementData赋值为EMPTY_ELEMENTDATA。
  2. 否则将elementData的大小调整为size。

总结

  1. ArrayList底层数据结构为数组。
  2. ArrayList默认初始化容量为10。可以通过调用ArrayList(int initialCapacity)构造函数传入指定的初始容量,当已知要使用的大概容量时,使用此构造函数可以节省ArrayList频繁扩容的开支。
  3. ArrayList在容量不够时会进行自动扩容,容量增加为原来的1.5倍。
  4. ArrayList不会自动缩容,但是可以通过调用trimToSize()方法进行手动缩容。
  5. ArrayList查询和访问效率较高,支持快速随机访问。
  6. ArrayList在列表尾部插入、删除元素时效率较高,但是被操作元素不在列表尾部时插入、删除效率较低,因为涉及到数组元素的迁移。
  7. 综上,ArrayList适合用于查询大于修改的场景。
  8. ArrayList非线程安全,多线程环境下不建议使用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值