[Java]-ArrayList 源码分析

前言

本文所查看的源码基于 JDK 1.8

RandomAccess 接口

ArrayList 实现了 RandomAccess 接口,另外 LinkedList 类则没有实现这个接口。这是个标志接口,只要 List 实现了这个接口,就能支持快速随机访问 (即通过索引访问)

例如 Collections 类中的 binarySearch 方法

public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
    if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
        return Collections.indexedBinarySearch(list, key);
    else
        return Collections.iteratorBinarySearch(list, key);
}

可以看出,如果 List 实现了RandomAccess接口,在遍历的时候就会采用基于索引的 传统for循环,否则就使用 迭代器遍历
也就是说在 JDK 的设计中,遍历 ArrayList 时偏向于采用 for 循环,遍历 LinkedList 时偏向于采用迭代器 iterator 遍历
因为 遍历 ArrayList 采用 for 循环会比使用迭代器快,而遍历 LinkedList 时采用迭代器 iterator 遍历会比使用 for 循环快
原因:ArrayList 是基于数组 (索引) 的存储结构,因此使用索引去获取一个元素的复杂度为 O(1),所以使用 for 进行遍历已经足够快,没有必要去借助迭代器花费额外的时间;
LinkedList 底层是基于双向链表实现的,使用索引获取单个元素的复杂度为 O(n),而使用迭代器遍历 LinkedList 的话是直接顺着链表节点的后继节点移动的,遍历所有节点的复杂度才是 O(n),所以使用迭代器遍历花费时间比使用 for 循环要少

主要成员变量

private static final int DEFAULT_CAPACITY = 10; //默认容量,在加入第一个元素时扩容会用到
private static final Object[] EMPTY_ELEMENTDATA = {}; //一个空数组
//也是一个空数组,与上面那个空数组的区别在ArrayList的构造函数以及存入第一个元素时再进行分析
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 
transient Object[] elementData; //存放数据元素的数组
private int size; //当前结构中存储的元素个数
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;//JDK设定的elementData数组最大大小

构造函数

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() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

当构造 ArrayList 有指定其大小时,如果指定的大小大于 0,就以用户指定的数值为 ArrayList 的大小;如果指定的数值小于 0 抛出异常;如果等于 0,就令存放数据的 elementData 直接指向 EMPTY_ELEMENTDATA

如果使用无参构造函数,就令 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;
    }
}

这个构造函数用于传入一个Collection集合,通过复制集合中的内容作为ArrayList的内容

扩容机制

说到 ArrayList 一般都会说到其底层的扩容机制,相关的方法有 add,grow 等等。接下来从 add 方法入手,模拟往 ArrayList 中添加元素时的扩容过程
首先,使用无参构造函数构造一个 ArrayList,那么其elementData会指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,然后调用如下的add方法加入第一个元素

add

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

要添加一个元素,先要判断当前 ArrayList 中的数据域 elementData 数组的大小是否足够多存放一个元素,即在正式添加这个元素前,应该 确保 elementData 的大小至少为当前 elementData 中 所存放元素的个数 size + 1
所以添加第一个元素时要求至少大小 minCapacity 为 1。这个确保工作用到了 ensureCapacityInternal 方法,其语义为 确保数组大小至少为 size + 1

ensureCapacityInternal

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

calculateCapacity

先看 calculateCapacity 方法

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData== DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

可以看出,这个方法主要是针对第一次添加元素,如果是 第一次添加元素,且创建 ArrayList 对象使用的是无参构造函数,那么默认第一次要扩容的大小为 DEFAULT_CAPACITY,即 10 (代码中选择的是 DEFAULT_CAPACITYminCapacity 中的最大值,但实际上第一次添加元素的时候 minCapacity 就是1)
这就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATAEMPTY_ELEMENTDATA 的区别所在,如果用户在创建 ArrayList 对象时使用的是有参构造函数指定了其初始容量,那么第一次扩容时就不会尝试扩容至 10,而是以用户指定的容量大小为主

我的理解是,如果用户使用的是无参构造函数,可以认为他对 ArrayList 的初始容量大小并没有什么要求,那么 JDK 在第一次扩容,索性就直接将容量扩充为 10,这样在添加第一到第十个元素的时候也不用去扩容,如果用户指定了初始容量,那么就以用户指定的为准,不去默认扩容,就算用户设置的初始容量也为 0,在添加前几个元素的时候可能会出现多几次的扩容操作,也不去管,完全尊重用户

ensureExplicitCapacity

接下来是 ensureExplicitCapacity 方法,Explicit 意为明确的,确实的,也就是说经过 calculateCapacity 方法决策后的最小容量才是确切需要的最小容量:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

overflow-conscious是 JDK 设计者考虑到溢出所设置的判断代码

判断所要求的最小容量 minCapacity 跟当前 elementData 的容量的关系,最小容量为 10,而当前elementData的容量为 0,所以要扩容,调用 grow 方法进行,此时 minCapacity 值为10

grow

private void grow(int minCapacity) {
    // overflow-consciouscode
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

默认新的容量大小 newCapacity 为旧容量大小的 1.5 倍,但如果 newCapacity 比所要求的最小容量 minCapacity 还小,就把 newCapacity 的值设为 minCapacity
然后如果 newCapacity 比设计的 elementData 数组最大大小 MAX_ARRAY_SIZE 还大,就调用hugeCapacity方法来计算最终要设置的新容量

那么一开始 newCapacity 为 0,minCapacity为 10,且 10 小于 MAX_ARRAY_SIZE,所以最终的 newCapacity 为 10,然后将 elementData 扩容至 10,最后回到 add 方法执行 elementData[size++] = e 语句,将添加的这第一个元素正式加入 elementData,同时 size 置为 1,表示当前整个 ArrayList 中存储的元素个数为 1,第一个元素添加成功

接下来添加第二个元素,传入 ensureCapacityInternal 方法的 minCapacity 参数为 2,经 calculateCapacity 方法决策后返回的还是 2,然后执行 ensureExplicitCapacity 方法,由于当前 elementData 的长度为 10,故不会经过扩容,直接回到add方法执行将元素放入 elementData 数组的操作。添加第三,四,五…十个元素的过程都是相同的

当添加第十一个元素时,传入 ensureCapacityInternal 方法的 minCapacity 参数为 11,经 calculateCapacity 方法后返回11,然后执行 ensureExplicitCapacity 方法,由于当前 elementData 的长度为 10 小于 11,所以会执行 grow(11),当前容量为 10,所以计算得到 newCapacity为 15,15 大于 11,所以最后 newCapacity 值为 15,这也就是说我们常说的 默认扩容 1.5倍

hugeCapacity

在 grow 方法中可以看到当 newCapacity 计算出的结果大于 MAX_ARRAY_SIZE,就需要执行 hugeCapacity 方法来确定 newCapacity 的最终结果

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

如果所要求的最小容量 minCapacity 也比 MAX_ARRAY_SIZE 还大,就直接取整型变量最大值为新容量的大小,否则取 MAX_ARRAY_SIZE 作为新容量的大小,确定了最终的新容量大小 newCapacity 的值

删除元素

删除元素主要涉及的方法为 remove(int index) 方法:

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

该方法首先使用 rangeCheck() 方法对索引 index 的合法性进行校验,然后获取被删除元素,作为方法最后的返回值
numMoved = size - index - 1 计算出删除元素后要复制的空间长度,因为每删除一个元素(除了删除最后一个元素)后都要把从被删除元素的后一个元素开始直到最后一个元素之间的所有元素向前移动一位
当 numMoved 大于0,就把 elementData 中最后 numMoved 个元素通过复制一起向前移动一位,最后返回被删除元素,方法执行完毕

关于 modCount

关于modCount 这个字段在 ArrayList 的父类 AbstractList 的源码中有详细的注释,大概意思就是:
这个字段记录这个 List 的结构被改变的次数 (类似于 CAS 中针对 “ABA” 问题有一种版本号 version 法,数据每被修改一次,version 加一),结构被改变,例如 List 的长度大小被改变,或其它可能导致迭代遍历的过程被影响的操作

这个字段被 iterator 以及 list iterator 的实现类所使用,如果它的值被意外修改,那么 iterator 就会在调用 next,remove,previous 等方法时抛出 ConcurrentModificationException,并发修改异常

这其实是一种 fast-fail 快速失败机制,防止在迭代时,由于并发修改而导致的不确定性行为,在检测到并发修改发生时,立即停止原有的操作。子类对这个字段的使用与否是可选的,如果子类希望迭代器 iterator 或 List iterator 能具有 fail-fast 机制,那么子类只需要在 add 方法,remove方法,以及其它会导致 List 结构被改变的方法中为这个字段添加增量操作即可

而 ArrayList 是使用了这个字段的。所以当我们使用迭代器遍历 (或增强 for 循环) 时,如果有其它线程并发对 List 进行了修改,就会抛出 ConcurrentModificationException

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值