集合——ArrayList源码

ArrayList ,基于 [] 数组实现的,支持自动扩容的动态数组。

 

 

 

 可以从图中看出,ArrayList实现了4个接口,继承了一个抽象类

每个类它所实现的接口、继承的抽象类都是有它这么做的意义:

  • java.util.List 接口,提供数组的添加、删除、修改、迭代遍历等操作。
  • java.util.RandomAccess 接口,表示 ArrayList 支持快速的随机访问。
  • java.io.Serializable 接口,表示 ArrayList 支持序列化的功能。
  • java.lang.Cloneable 接口,表示 ArrayList 支持克隆。

 

  • java.util.AbstractList 抽象类,提供了 List 接口的骨架实现,大幅度的减少了实现迭代遍历相关操作的代码。

 

属性:

  

 

 

 

  • elementData 属性:元素数组。其中,图中红色空格代表我们已经添加元素,白色空格代表我们并未使用。
  • size 属性:数组大小。注意,size 代表的是 ArrayList 已使用 elementData 的元素的数量,对于开发者看到的 #size() 也是该大小。并且,当我们添加新的元素时,恰好其就是元素添加到 elementData 的位置(下标)。当然, ArrayList 真正的大小是 elementData 的大小。

 

/**
 * 元素数组。
 *
 * 当添加新的元素时,如果该数组不够,会创建新数组,并将原数组的元素拷贝到新数组。之后,将该变量指向新数组。
 *
 * The array buffer into which the elements of the ArrayList are stored.
 * The capacity of the ArrayList is the length of this array buffer. Any
 * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
 * will be expanded to DEFAULT_CAPACITY when the first element is added.
 */
transient Object[] elementData; // non-private to simplify nested class access 不使用 private 修复,方便内嵌类的访问。

/**
 * 已使用的数组大小
 *
 * The size of the ArrayList (the number of elements it contains).
 *
 * @serial
 */
private int size;

 

构造方法:

ArrayList 一共有三个构造方法:

1.ArrayList(int initialCapacity)根据传入的初始化容量,创建 ArrayList 数组。如果我们在使用时,如果预先指到数组大小,一定要使用该构造方法,可以避免数组扩容提升性能,同时也是合理使用内存。

/**
 * 共享的空数组对象。
 *
 * 在 {@link #ArrayList(int)} 或 {@link #ArrayList(Collection)} 构造方法中,
 * 如果传入的初始化大小或者集合大小为 0 时,将 {@link #elementData} 指向它。
 *
 * Shared empty array instance used for empty instances.
 */
private static final Object[] EMPTY_ELEMENTDATA = {};

public ArrayList(int initialCapacity) {
    // 初始化容量大于 0 时,创建 Object 数组
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    // 初始化容量等于 0 时,使用 EMPTY_ELEMENTDATA 对象
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    // 初始化容量小于 0 时,抛出 IllegalArgumentException 异常
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
  • 比较特殊的是,如果初始化容量为 0 时,使用 EMPTY_ELEMENTDATA 空数组。在添加元素的时候,会进行扩容创建需要的数组。

 

2.ArrayList(Collection<? extends E> c)

  使用传入的 c 集合,作为 ArrayList 的 elementData。

public ArrayList(Collection<? extends E> c) {
    // 将 c 转换成 Object 数组
    elementData = c.toArray();
    // 如果数组大小大于 0
    if ((size = elementData.length) != 0) {
        // defend against c.toArray (incorrectly) not returning Object[]
        // (see e.g. https://bugs.openjdk.java.net/browse/JDK-6260652)
        // <X> 如果集合元素不是 Object[] 类型,则会创建新的 Object[] 数组,并将 elementData 赋值到其中,最后赋值给 elementData 。
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    // 如果数组大小等于 0 ,则使用 EMPTY_ELEMENTDATA 。
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
  • 比较让人费解的是,在 <X> 处的代码。它是用于解决 JDK-6260652 的 Bug 。它在 JDK9 中被解决,😈 也就是说,JDK8 还会存在该问题。

 

触发 JDK-6260652 的测试代码,然后分别在 JDK8 和 JDK13 下执行。代码如下:

public static void test02() {
    List<Integer> list = Arrays.asList(1, 2, 3);
    Object[] array = list.toArray(); // JDK8 返回 Integer[] 数组,JDK9+ 返回 Object[] 数组。
    System.out.println("array className :" + array.getClass().getSimpleName());

    // 此处,在 JDK8 和 JDK9+ 表现不同,前者会报 ArrayStoreException 异常,后者不会。
    array[0] = new Object();
}

 

JDK8 执行如下图所示:

 

 

 

JDK13 执行如下图所示:

 

 

 

 

 

 

  • 在 JDK8 中,返回的实际是 Integer [] 数组,那么我们将 Object 对象设置到其中,肯定是会报错的。具体怎么修复的,看 JDK-6260652 的最末尾一段(Only use .toArray(Object[]))?????。

 

3.ArrayList()

  使用最多的构造方法:

/**
 * 默认初始化容量
 *
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;

/**
 * 共享的空数组对象,用于 {@link #ArrayList()} 构造方法。
 *
 * 通过使用该静态变量,和 {@link #EMPTY_ELEMENTDATA} 区分开来,在第一次添加元素时。
 *
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

 

  • 在我们学习 ArrayList 的时候,一直被灌输了一个概念,在未设置初始化容量时,ArrayList 默认大小为 10 。但是此处,我们可以看到初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这个空数组。这是为什么呢?ArrayList 考虑到节省内存,一些使用场景下仅仅是创建了 ArrayList 对象,实际并未使用。所以,ArrayList 优化成初始化是个空数组,在首次添加元素时,才真正初始化为容量为 10 的数组。
  • 那么为什么单独声明了 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组,而不直接使用 EMPTY_ELEMENTDATA 呢?在下文中,我们会看到 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 首次扩容为 10 ,而 EMPTY_ELEMENTDATA 按照 1.5 倍扩容从 0 开始而不是 10 。😈 两者的起点不同,嘿嘿。

 

添加单个元素:

  add(E e) 方法,顺序添加单个元素到数组。代码如下:

@Override
public boolean add(E e) {
    // <1> 增加数组修改次数
    modCount++;
    // 添加元素
    add(e, elementData, size);
    // 返回添加成功
    return true;
}

private void add(E e, Object[] elementData, int s) {
    // <2> 如果容量不够,进行扩容
    if (s == elementData.length)
        elementData = grow();
    // <3> 设置到末尾
    elementData[s] = e;
    // <4> 数量大小加一
    size = s + 1;
}
  • <1> 处,增加数组修改次数 modCount 。在父类 AbstractList 上,定义了 modCount 属性,用于记录数组修改次数。
  • <2> 处,如果元素添加的位置就超过末尾(数组下标是从 0 开始,而数组大小比最大下标大 1),说明数组容量不够,需要进行扩容,那么就需要调用 grow() 方法,进行扩容。
  • <3> 处,设置到末尾。
  • <4> 处,数量大小加一。

add(int index, E element) 方法,插入单个元素到指定位置。代码如下:

public void add(int index, E element) {
    // 校验位置是否在数组范围内
    rangeCheckForAdd(index);
    // 增加数组修改次数
    modCount++;
    // 如果数组大小不够,进行扩容
    final int s;
    Object[] elementData;
    if ((s = size) == (elementData = this.elementData).length)
        elementData = grow();
    // 将 index + 1 位置开始的元素,进行往后挪
    System.arraycopy(elementData, index,
                     elementData, index + 1,
                     s - index);
    // 设置到指定位置
    elementData[index] = element;
    // 数组大小加一
    size = s + 1;
}

private void rangeCheckForAdd(int index) {
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

 

数组扩容:

  重点!!!不同构造初始化第一次扩容时:

  在调用ArrayList(int initialCapacity)初始化或调ArrayList()初始化,在第一次添加元素的时候都会调用扩容方法

  (PS:此时,它们第一次的扩容方式不同:

      前者一般情况下是1.5倍扩容, 特殊情况1.构造传参为0时使用的是最小长度1,另一种特殊情况,添加元素大于0.5倍长度的元素时扩容会大于1.5;

      后者第一次初始化如果添加元素长度小于默认长度10,则直接使用10作为数组长度,如果大于10则直接扩容至刚到放下所有元素的长度,之后才1.5倍扩容,同样也不是绝对1.5倍扩容也存在上述第二种特殊情况)

 

 

  grow() 方法,扩容数组,并返回它。整个的扩容过程,首先创建一个新的更大的数组,一般是 1.5 倍大小(为什么说是一般呢,稍后会看到,会有一些小细节),然后将原数组复制到新数组中,最后返回新数组。代码如下:

private Object[] grow() {
    // <1>
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    // <2> 如果原容量大于 0 ,或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,计算新的数组大小,并创建扩容
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    // <3> 如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}
  • <1> 处,调用 #grow(int minCapacity) 方法,要求扩容后至少比原有大 1 。因为是最小扩容的要求,实际是允许比它大。
  • <2> 处,如果原容量大于 0 时,又或者数组不是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,则计算新的数组大小,并创建扩容。
    • ArraysSupport#newLength(int oldLength, int minGrowth, int prefGrowth) 方法,计算新的数组大小。简单来说,结果就是 Math.max(minGrowth, prefGrowth) + oldLength ,按照 minGrowth 和 prefGrowth 取大的。
    • 一般情况下,从 oldCapacity >> 1 可以看处,是 1.5 倍扩容。但是会有两个特殊情况:1)初始化数组要求大小为 0 的时候,0 >> 1 时(>> 1 为右移操作,相当于除以 2)还是 0 ,此时使用 minCapacity 传入的 1 。2)在下文中,我们会看到添加多个元素,此时传入的 minCapacity 不再仅仅加 1 ,而是扩容到 elementData 数组恰好可以添加下多个元素,而该数量可能会超过当前 ArrayList 0.5 倍的容量。
  • <3> 处,如果是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,直接创建新的数组即可。思考下,如果无参构造方法使用 EMPTY_ELEMENTDATA 的话,无法实现该效果了。

 

既然有数组扩容方法,那么是否有缩容方法呢?在 trimToSize() 方法中,会创建大小恰好够用的新数组,并将原数组复制到其中。代码如下:

public void trimToSize() {
    // 增加修改次数
    modCount++;
    // 如果有多余的空间,则进行缩容
    if (size < elementData.length) {
        elementData = (size == 0)
          ? EMPTY_ELEMENTDATA // 大小为 0 时,直接使用 EMPTY_ELEMENTDATA
          : Arrays.copyOf(elementData, size); // 大小大于 0 ,则创建大小为 size 的新数组,将原数组复制到其中。
    }
}

同时,提供 ensureCapacity(int minCapacity) 方法,保证 elementData 数组容量至少有 minCapacity 。代码如下:

public void ensureCapacity(int minCapacity) {
    if (minCapacity > elementData.length // 如果 minCapacity 大于数组的容量
        && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
             && minCapacity <= DEFAULT_CAPACITY)) { // 如果 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的时候,
                                                    // 需要最低 minCapacity 容量大于 DEFAULT_CAPACITY ,因为实际上容量是 DEFAULT_CAPACITY 。
        // 数组修改次数加一
        modCount++;
        // 扩容
        grow(minCapacity);
    }
}
  • 比较简单,可以将这个方法理解成主动扩容。

 

添加多个元素:

  addAll(Collection<? extends E> c) 方法,批量添加多个元素。在明确知道会添加多个元素时,推荐使用该该方法而不是添加单个元素,避免可能多次扩容。代码如下:

public boolean addAll(Collection<? extends E> c) {
    // 转成 a 数组
    Object[] a = c.toArray();
    // 增加修改次数
    modCount++;
    // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化
    int numNew = a.length;
    if (numNew == 0)
        return false;
    // <1> 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。
    Object[] elementData;
    final int s;
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);
    // <2> 将 a 复制到 elementData 从 s 开始位置
    System.arraycopy(a, 0, elementData, s, numNew);
    // 数组大小加 numNew
    size = s + numNew;
    return true;
}
  • <1> 处,如果 elementData 剩余的空间不足,则进行扩容。要求扩容的大小,至于能够装下 a 数组。在 前面「数组扩容」 ,已经知道,如果要求扩容的空间太小,则扩容 1.5 倍
  • <2> 处,将 a 复制到 elementData 从 s 开始位置。

总的看下来,就是 add(E e) 方法的批量版本,优势就正如前面说的,避免可能多次扩容。

 

addAll(int index, Collection<? extends E> c) 方法,从指定位置开始插入多个元素。代码如下:

public boolean addAll(int index, Collection<? extends E> c) {
    // 校验位置是否在数组范围内
    rangeCheckForAdd(index);

    // 转成 a 数组
    Object[] a = c.toArray();
    // 增加数组修改次数
    modCount++;
    // 如果 a 数组大小为 0 ,返回 ArrayList 数组无变化
    int numNew = a.length;
    if (numNew == 0)
        return false;
    // 如果 elementData 剩余的空间不够,则进行扩容。要求扩容的大小,至于能够装下 a 数组。
    Object[] elementData;
    final int s;
    if (numNew > (elementData = this.elementData).length - (s = size))
        elementData = grow(s + numNew);

    // 【差异点】如果 index 开始的位置已经被占用,将它们后移
    int numMoved = s - index;
    if (numMoved > 0)
        System.arraycopy(elementData, index,
                         elementData, index + numNew,
                         numMoved);

    // 将 a 复制到 elementData 从 s 开始位置
    System.arraycopy(a, 0, elementData, index, numNew);
    // 数组大小加 numNew
    size = s + numNew;
    return true;
}

 

移除单个元素:

  remove(int index) 方法,移除指定位置的元素,并返回该位置的原元素。代码如下:

public E remove(int index) {
    // 校验 index 不要超过 size
    Objects.checkIndex(index, size);
    final Object[] es = elementData;

    // 记录该位置的原值
    @SuppressWarnings("unchecked") E oldValue = (E) es[index];
    // <X>快速移除
    fastRemove(es, index);

    // 返回该位置的原值
    return oldValue;
}

重点是 <X> 处,调用 #fastRemove(Object[] es, int i) 方法,快速移除。代码如下:

private void fastRemove(Object[] es, int i) {
    // 增加数组修改次数
    modCount++;
    // <Y>如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪
    final int newSize;
    if ((newSize = size - 1) > i) // -1 的原因是,size 是从 1 开始,而数组下标是从 0 开始。
        System.arraycopy(es, i + 1, es, i, newSize - i); //newSize - i 是 移动的元素的个数,从(i+1)开始的(newSize - i)元素前移
// 将新的末尾置为 null ,帮助 GC es[size = newSize] = null; }
  • <Y> 处,看起来比较复杂,胖友按照“如果 i 不是移除最末尾的元素,则将 i + 1 位置的数组往前挪”来理解,就很好懂了。

 

remove(Object o) 方法,移除首个为 o 的元素,并返回是否移除到。代码如下:

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    // <Z> 寻找首个为 o 的位置
    int i = 0;
    found: {
        if (o == null) { // o 为 null 的情况
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else { // o 非 null 的情况
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        // 如果没找到,返回 false
        return false;
    }
    // 快速移除
    fastRemove(es, i);
    // 找到了,返回 true
    return true;
}
  • 和 remove(int index) 差不多,就是在 <Z> 处,改成获得首个为 o 的位置,之后就调用 fastRemove(Object[] es, int i) 方法,快速移除即可。

 

移除多个元素:

  removeRange(int fromIndex, int toIndex) 方法,批量移除 [fromIndex, toIndex) 的多个元素,注意不包括 toIndex 的元素噢。代码如下:

protected void removeRange(int fromIndex, int toIndex) {
    // 范围不正确,抛出 IndexOutOfBoundsException 异常
    if (fromIndex > toIndex) {
        throw new IndexOutOfBoundsException(
                outOfBoundsMsg(fromIndex, toIndex));
    }
    // 增加数组修改次数
    modCount++;
    // <X> 移除 [fromIndex, toIndex) 的多个元素
    shiftTailOverGap(elementData, fromIndex, toIndex);
}

private static String outOfBoundsMsg(int fromIndex, int toIndex) {
    return "From Index: " + fromIndex + " > To Index: " + toIndex;
}

 

<X> 处,调用 #shiftTailOverGap(Object[] es, int lo, int hi) 方法,移除 [fromIndex, toIndex) 的多个元素。代码如下:

private void shiftTailOverGap(Object[] es, int lo, int hi) {
    // 将 es 从 hi 位置开始的元素,移到 lo 位置开始。
    System.arraycopy(es, hi, es, lo, size - hi);
    // 将从 [size - hi + lo, size) 的元素置空,因为已经被挪到前面了。
    for (int to = size, i = (size -= hi - lo); i < to; i++)
        es[i] = null;
}
  • 和 #fastRemove(Object[] es, int i) 方法一样的套路,先挪后置 null 。
  • 有一点要注意,ArrayList 特别喜欢把多行代码写成一行。所以,可能会有疑惑,貌似这里没有修改数组的大小 size 啊?答案在 i = (size -= hi - lo) ,简直到精简到难懂。

 

removeAll(Collection<?> c) 方法,批量移除指定的多个元素。实现逻辑比较简单,但是看起来会比较绕。(PS:但是这里面采用的优化遍历技巧很实用,感觉不少场景都很适用,且面试也经常会有这种类似遍历场景)简单来说,通过两个变量 w(写入位置)和 r(读取位置),按照 r 顺序遍历数组(elementData),如果不存在于指定的多个元素中,则写入到 elementData 的 w 位置,然后 w 位置 + 1 ,跳到下一个写入位置。通过这样的方式,实现将不存在 elementData 覆盖写到 w 位置。可能理解起来有点绕,当然看代码也会有点绕绕。代码如下:

boolean batchRemove(Collection<?> c, boolean complement, final int from, final int end) {
    // 校验 c 非 null 。
    Objects.requireNonNull(c);
    final Object[] es = elementData;
    int r;
    // Optimize for initial run of survivors
    // <1> 优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。
    for (r = from;; r++) {
        // <1.1> 遍历到尾,都没不符合条件的,直接返回 false 。
        if (r == end)
            return false;
        // <1.2> 如果包含结果不符合 complement 时,结束
        if (c.contains(es[r]) != complement)
            break;
    }
    // <2> 设置开始写入 w 为 r ,注意不是 r++ 。
    // r++ 后,用于读取下一个位置的元素。因为通过上的优化循环,我们已经 es[r] 是不符合条件的。
    int w = r++;
    try {
        // <3> 继续遍历 elementData 数组,如何符合条件,则进行移除
        for (Object e; r < end; r++)
            if (c.contains(e = es[r]) == complement) // 判断符合条件
                es[w++] = e; // 移除的方式,通过将当前值 e 写入到 w 位置,然后 w 跳到下一个位置。
    } catch (Throwable ex) {
        // Preserve behavioral compatibility with AbstractCollection,
        // even if c.contains() throws.
        // <4> 如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置
        System.arraycopy(es, r, es, w, end - r);
        w += end - r;
        // 继续抛出异常
        throw ex;
    } finally { // <5>
        // 增加数组修改次数
        modCount += end - w;
        // 将数组 [w, end) 位置赋值为 null 。
        shiftTailOverGap(es, w, end);
    }
    return true;
}
  • 先看下每一小块的逻辑。然后,调试下,妥妥的就明白了。
  • complement 参数,翻译过来是“补足”的意思。怎么理解呢?表示如果 elementData 元素在 c 集合中时,是否保留。
    • 如果 complement 为 false 时,表示在集合中,就不保留,这显然符合 removeAll(Collection<?> c) 方法要移除的意图。
    • 如果 complement 为 true 时,表示在集合中,就暴露,这符合我们后面会看到的 retainAll(Collection<?> c) 方法要求交集的意图。
  • <1> 处,首先我们要知道这是一个基于 Optimize 优化的目的。我们是希望先判断是否 elementData 没有任何一个符合 c 的,这样就无需进行执行对应的移除逻辑。但是,我们又希望能够避免重复遍历,于是就有了这样一块的逻辑。总的来说,这块逻辑的目的是,优化,顺序遍历 elementData 数组,找到第一个不符合 complement ,然后结束遍历。
    • <1.1> 处,遍历到尾,都没不符合条件的,直接返回 false 。也就是说,丫根就不需要进行移除的逻辑。
    • <1.2> 处,如果包含结果不符合 complement 时,结束循环。可能有点难理解,我们来举个例子。假设 elementData 是 [1, 2, 3, 1] 时,c 是 [2] 时,那么在遍历第 0 个元素 1 时,则 c.contains(es[r]) != complement => false != false 不符合,所以继续缓存;然后,在遍历第 1 个元素 2 时,c.contains(es[r]) != complement => true != false 符合,所以结束循环。此时,我们便找到了第一个需要移除的元素的位置。当然,移除不是在这里执行
  • <2> 处,设置开始写入 w 为 r ,注意不是 r++ 。这样,我们后续在循环 elementData 数组,就会从 w 开始写入。并且此时,r 也跳到了下一个位置,这样间接我们可以发现,w 位置的元素已经被“跳过”了。
  • <3> 处,继续遍历 elementData 数组,如何符合条件,则进行移除。可能有点难理解,我们继续上述例子。遍历第 2 个元素 3 时候,c.contains(es[r]) == complement => false == false 符合,所以将 3 写入到 w 位置,同时 w 指向下一个位置;遍历第三个元素 1 时候,c.contains(es[r]) == complement => true == false 不符合,所以不进行任何操作。
  • <4> 处,如果 contains 方法发生异常,则将 es 从 r 位置的数据写入到 es 从 w 开始的位置。这样,保证我们剩余未遍历到的元素,能够挪到从从 w 开始的位置,避免多出来一些元素。
  • <5> 处,是不是很熟悉,将数组 [w, end) 位置赋值为 null 。

 

retainAll(Collection<?> c) 方法,求 elementData 数组和指定多个元素的交集。简单来说,恰好和 removeAll(Collection<?> c) 相反,移除不在 c 中的元素。代码如下:

public boolean retainAll(Collection<?> c) {
    return batchRemove(c, true, 0, size);
}

 

查找单个元素:

indexOf(Object o) 方法,查找首个为指定元素的位置。代码如下:

public int indexOf(Object o) {
    return indexOfRange(o, 0, size);
}

int indexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    // o 为 null 的情况
    if (o == null) {
        for (int i = start; i < end; i++) {
            if (es[i] == null) {
                return i;
            }
        }
    // o 非 null 的情况
    } else {
        for (int i = start; i < end; i++) {
            if (o.equals(es[i])) {
                return i;
            }
        }
    }
    // 找不到,返回 -1
    return -1;
}

 

contains(Object o) 方法,就是基于该方法实现。代码如下:

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

 

有时我们需要查找最后一个为指定元素的位置,所以会使用到 #lastIndexOf(Object o) 方法。代码如下:

public int lastIndexOf(Object o) {
    return lastIndexOfRange(o, 0, size);
}

int lastIndexOfRange(Object o, int start, int end) {
    Object[] es = elementData;
    // o 为 null 的情况
    if (o == null) {
        for (int i = end - 1; i >= start; i--) { // 倒序
            if (es[i] == null) {
                return i;
            }
        }
    // o 非 null 的情况
    } else {
        for (int i = end - 1; i >= start; i--) { // 倒序
            if (o.equals(es[i])) {
                return i;
            }
        }
    }

    // 找不到,返回 -1
    return -1;
}

 

获得指定位置的元素:

get(int index) 方法,获得指定位置的元素。代码如下:

public E get(int index) {
    // 校验 index 不要超过 size
    Objects.checkIndex(index, size);
    // 获得 index 位置的元素
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}
  • 随机访问 index 位置的元素,时间复杂度为 O(1) 。

 

设置指定位置的元素:

set(int index, E element) 方法,设置指定位置的元素。代码如下:

public E set(int index, E element) {
    // 校验 index 不要超过 size
    Objects.checkIndex(index, size);
    // 获得 index 位置的原元素
    E oldValue = elementData(index);
    // 修改 index 位置为新元素
    elementData[index] = element;
    // 返回 index 位置的原元素
    return oldValue;
}

 

转换成数组:

public Object[] toArray() {
    return Arrays.copyOf(elementData, size);
}

// Arrays.java

public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}
  • 注意,返回的是 Object[] 类型噢。

实际场景下,可能想要指定 T 泛型的数组,那么就需要使用到 toArray(T[] a) 方法。代码如下:

public <T> T[] toArray(T[] a) {
    // <1> 如果传入的数组小于 size 大小,则直接复制一个新数组返回
    if (a.length < size)
        // Make a new array of a's runtime type, but my contents:
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    // <2> 将 elementData 复制到 a 中
    System.arraycopy(elementData, 0, a, 0, size);
    // <2.1> 如果传入的数组大于 size 大小,则将 size 赋值为 null
    if (a.length > size)
        a[size] = null;
    // <2.2> 返回 a
    return a;
}
  • 分成 2 个情况,根据传入的 a 数组是否足够大。
  • <1> 处,如果传入的数组小于 size 大小,则直接复制一个新数组返回。一般情况下,不会这么干。
  • <2> 处,将 elementData 复制到 a 中。
    • <2.1> 处,如果传入的数组大于 size 大小,则将 size 位置赋值为 null 。额,有点没搞懂这个有啥目的。
    • <2.2> 处,返回传入的 a 。很稳。
  • 考虑到 <1> 处,可能会返回一个新数组,所以即使 <2> 返回的就是 a 数组,最好使用还是按照 a = list.toArray(a) 。

 

 克隆:

public Object clone() {
    try {
        // 调用父类,进行克隆
        ArrayList<?> v = (ArrayList<?>) super.clone();
        // 拷贝一个新的数组
        v.elementData = Arrays.copyOf(elementData, size);
        // 设置数组修改次数为 0
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

elementData 是重新拷贝出来的新的数组,避免和原数组共享。

 

总结:

  • ArrayList 是基于 [] 数组实现的 List 实现类,支持在数组容量不够时,一般按照 1.5 倍自动扩容。同时,它支持手动扩容、手动缩容。
  • ArrayList 随机访问时间复杂度是 O(1) ,查找指定元素的平均时间复杂度是 O(n) 。
  • ArrayList 移除指定位置的元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。--------最好时间复杂度发生在末尾移除的情况。
  • ArrayList 移除指定元素的时间复杂度是 O(n) 。--------因为首先需要进行查询,然后在使用移除指定位置的元素,无论怎么计算,都需要 O(n) 的时间复杂度。
  • ArrayList 添加元素的最好时间复杂度是 O(1) ,最坏时间复杂度是 O(n) ,平均时间复杂度是 O(n) 。--------最好时间复杂度发生在末尾添加的情况。

 

对时间复杂度的计算方式可以参考以下博客:

《算法复杂度分析(上):分析算法运行时,时间资源及空间资源的消耗》 

《算法复杂度分析(下):最好、最坏、平均、均摊等时间复杂度概述》

 

posted @ 2021-04-11 18:08  X凯  阅读(127)  评论(0编辑  收藏  举报