ArrayList 源码详细分析

1.概述数组

ArrayList 是一种变长的集合类,    ,当往 ArrayList 中添加的元素数量大于其底层数组容量时,其会经过扩容机制从新生成一个更大的数组。另外,因为 ArrayList 底层基于数组实现,因此其能够保证在 O(1) 复杂度下完成随机查找操做。其余方面,ArrayList 是非线程安全类,并发环境下,多个线程同时操做 ArrayList,会引起不可预知的错误。安全

ArrayList 是你们最为经常使用的集合类,做为一个变长集合类,其核心是扩容机制。因此只要知道它是怎么扩容的,以及基本的操做是怎样实现就够了。本文后续内容也将围绕这些点展开叙述。数据结构

2.源码分析并发

2.1 构造方法

ArrayList 有两个构造方法,一个是无参,另外一个需传入初始容量值。你们平时最经常使用的是无参构造方法,相关代码以下:框架

private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

transient Object[] 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;
}

上面的代码比较简单,两个构造方法作的事情并不复杂,目的都是初始化底层数组 elementData。区别在于无参构造方法会将 elementData 初始化一个空数组,插入元素时,扩容将会按默认值从新初始化数组。而有参的构造方法则会将 elementData 初始化为参数值大小(>= 0)的数组。通常状况下,咱们用默认的构造方法便可。假若在可知道将会向 ArrayList 插入多少元素的状况下,应该使用有参构造方法。按需分配,避免浪费。dom

2.2 插入

对于数组(线性表)结构,插入操做分为两种状况。一种是在元素序列尾部插入,另外一种是在元素序列其余位置插入。ArrayList 的源码里也体现了这两种插入状况,以下:源码分析

对于在元素序列尾部插入,这种状况比较简单,只需两个步骤便可:ui

  1. 检测数组是否有足够的空间插入
  2. 将新元素插入至序列尾部

以下图:this

若是是在元素序列指定位置(假设该位置合理)插入,则状况稍微复杂一点,须要三个步骤:spa

  1. 检测数组是否有足够的空间
  2. 将 index 及其以后的全部元素向后移一位
  3. 将新元素插入至 index 处

以下图:

从上图能够看出,将新元素插入至序列指定位置,须要先将该位置及其以后的元素都向后移动一位,为新元素腾出位置。这个操做的时间复杂度为O(N),频繁移动元素可能会致使效率问题,特别是集合中元素数量较多时。在平常开发中,若非所需,咱们应当尽可能避免在大集合中调用第二个插入方法。

以上是 ArrayList 插入相关的分析,上面的分析以及配图均未体现扩容机制。那么下面就来简单分析一下 ArrayList 的扩容机制。对于变长数据结构,当结构中没有空余空间可供使用时,就须要进行扩容。在 ArrayList 中,当空间用完,其会按照原数组空间的1.5倍进行扩容。相关源码以下:

/** 计算最小容量 */
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

/** 扩容的入口方法 */
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

/** 扩容的核心方法 */
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // newCapacity = oldCapacity + oldCapacity / 2 = oldCapacity * 1.5
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    // 若是最小容量超过 MAX_ARRAY_SIZE,则将数组容量扩容至 Integer.MAX_VALUE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

上面就是扩容的逻辑,代码虽多,但不少都是边界检查,这里就不详细分析了。

2.3 删除

不一样于插入操做,ArrayList 没有无参删除方法。因此其只能删除指定位置的元素或删除指定元素,这样就没法避免移动元素(除非从元素序列的尾部删除)。相关代码以下:

/** 删除指定位置的元素 */
public E remove(int index) {
    rangeCheck(index);

    modCount++;
    // 返回被删除的元素值
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        // 将 index + 1 及以后的元素向前移动一位,覆盖被删除值
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    // 将最后一个元素置空,并将 size 值减1                
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

E elementData(int index) {
    return (E) elementData[index];
}

/** 删除指定元素,若元素重复,则只删除下标最小的元素 */
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;
}

/** 快速删除,不作边界检查,也不返回删除的元素值 */
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
}

上面的删除方法并不复杂,这里以第一个删除方法为例,删除一个元素步骤以下:

  1. 获取指定位置 index 处的元素值
  2. 将 index + 1 及以后的元素向前移动一位
  3. 将最后一个元素置空,并将 size 值减 1
  4. 返回被删除值,完成删除操做

以下图:

上面就是删除指定位置元素的分析,并非很复杂。

如今,考虑这样一种状况。咱们往 ArrayList 插入大量元素后,又删除不少元素,此时底层数组会空闲处大量的空间。由于 ArrayList 没有自动缩容机制,致使底层数组大量的空闲空间不能被释放,形成浪费。对于这种状况,ArrayList 也提供了相应的处理方法,以下:

/** 将数组容量缩小至元素数量 */
public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size);
    }
}

经过上面的方法,咱们能够手动触发 ArrayList 的缩容机制。这样就能够释放多余的空间,提升空间利用率。

2.4 遍历

ArrayList 实现了 RandomAccess 接口(该接口是个标志性接口),代表它具备随机访问的能力。ArrayList 底层基于数组实现,因此它可在常数阶的时间内完成随机访问,效率很高。对 ArrayList 进行遍历时,通常状况下,咱们喜欢使用 foreach 循环遍历,但这并非推荐的遍历方式。ArrayList 具备随机访问的能力,若是在一些效率要求比较高的场景下,更推荐下面这种方式:

for (int i = 0; i < list.size(); i++) {
    list.get(i);
}

至于缘由也不难理解,foreach 最终会被转换成迭代器遍历的形式,效率不如上面的遍历方式。

3.其余细节

3.1 快速失败机制

在 Java 集合框架中,不少类都实现了快速失败机制。该机制被触发时,会抛出并发修改异常ConcurrentModificationException,这个异常你们在平时开发中多多少少应该都碰到过。关于快速失败机制,ArrayList 的注释里对此作了解释,这里引用一下:

The iterators returned by this class's iterator() and
listIterator(int) methods are fail-fast
if the list is structurally modified at any time after the iterator is
created, in any way except through the iterator's own
ListIterator remove() or ListIterator add(Object) methods,
the iterator will throw a ConcurrentModificationException. Thus, in the face of
concurrent modification, the iterator fails quickly and cleanly, rather
than risking arbitrary, non-deterministic behavior at an undetermined
time in the future.

上面注释大体意思是,ArrayList 迭代器中的方法都是均具备快速失败的特性,当遇到并发修改的状况时,迭代器会快速失败,以免程序在未来不肯定的时间里出现不肯定的行为。

以上就是 Java 集合框架中引入快速失败机制的缘由,并不难理解,这里很少说了。

3.2 关于遍历时删除

遍历时删除是一个不正确的操做,即便有时候代码不出现异常,但执行逻辑也会出现问题。关于这个问题,阿里巴巴 Java 开发手册里也有所说起。这里引用一下:

【强制】不要在 foreach 循环里进行元素的 remove/add 操做。remove 元素请使用 Iterator 方式,若是并发操做,须要对 Iterator 对象加锁。

相关代码(稍做修改)以下:

List<String> a = new ArrayList<String>();
    a.add("1");
    a.add("2");
    for (String temp : a) {
        System.out.println(temp);
        if("1".equals(temp)){
            a.remove(temp);
        }
    }
}

相信有些朋友应该看过这个,而且也执行过上面的程序。上面的程序执行起来不会虽不会出现异常,但代码执行逻辑上却有问题,只不过这个问题隐藏的比较深。咱们把 temp 变量打印出来,会发现只打印了数字12没打印出来。初看这个执行结果确实很让人诧异,不明缘由。若是死抠上面的代码,咱们很难找出缘由,此时须要稍微转换一下思路。咱们都知道 Java 中的 foreach 是个语法糖,编译成字节码后会被转成用迭代器遍历的方式。因此咱们能够把上面的代码转换一下,等价于下面形式:

List<String> a = new ArrayList<>();
a.add("1");
a.add("2");
Iterator<String> it = a.iterator();
while (it.hasNext()) {
    String temp = it.next();
    System.out.println("temp: " + temp);
    if("1".equals(temp)){
        a.remove(temp);
    }
}

这个时候,咱们再去分析一下 ArrayList 的迭代器源码就能找出缘由。

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

    // 省略不相关的代码
}

咱们一步一步执行一下上面的代码,第一次进入 while 循环时,一切正常,元素 1 也被删除了。但删除元素 1 后,就没法再进入 while 循环,此时 it.hasNext() 为 false。缘由是删除元素 1 后,元素计数器 size = 1,而迭代器中的 cursor 也等于 1,从而致使 it.hasNext() 返回false。归根结底,上面的代码段没抛异常的缘由是,循环提早结束,致使 next 方法没有机会抛异常。不信的话,你们能够把代码稍微修改一下,便可发现问题:

List<String> a = new ArrayList<>();
a.add("1");
a.add("2");
a.add("3");
Iterator<String> it = a.iterator();
while (it.hasNext()) {
    String temp = it.next();
    System.out.println("temp: " + temp);
    if("1".equals(temp)){
        a.remove(temp);
    }
}

以上是关于遍历时删除的分析,在平常开发中,咱们要避免上面的作法。正确的作法使用迭代器提供的删除方法,而不是直接删除。

4.总结

 ArrayList 就是一个比较基础的集合类,用的不少。它的结构简单(本质上就是一个变长的数组),实现上也不复杂,感谢阅读。

相关文章
相关标签/搜索