#数据结构-ArrayList 源码分析

300 阅读6分钟
  • 在逻辑结构上是:线性结构,线性表
  • 在物理结构是:连续储存结构

实现的接口

ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable

  • RandomAccess

    RandomAccess,没有方法,是一个标记的接口。List 实现这个接口用来表示其支持随机访问:即下标访问,(通常是固定时间RandomAccess 这个标记接口就是标记能够随机访问元素的集合, 简单来说就 是底层是数组实现的集合。 for (int i=0, n=list.size(); i < n; i++) 循环,比 for (Iterator i=list.iterator(); i.hasNext(); )i.next(); 要快。(快速随机访问速度 > 迭代器遍历速度。)

  • Cloneable 空接口,实现了这个接口,在使用 Object.clone() 方法进行每个字段的拷贝是合法的,没有实现这个方法会抛出 CloneNotSupportedException。接口不包含 clone 方法,及时这个实现了这个接口,也不能进行 clone,即使是使用反射。实现此接口的类应该使用公共方法覆盖 clone 方法.

    引申思考 深克隆和浅克隆的区别?

ArrayList 属性

// 默认长度
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小空实例的共享空数组实例,与 EMPTY_ELEMENTDATA 的区别在于知道当第一个元素插入时该如何扩充数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
// 保存了多少个数据
private int size;
// 最大的长度,2^31 - 8 减去头关键字
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

构造方法 3 个

// 有初始容量
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;
}
// 使用其他集合来初始化
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            // 调用 Array.copyOf
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

扩容机制

在初始化 ArrayList 的时候,JVM 并不知道要存多少个数据,但是数组必须要声明其大小。这样的化,最迟在第一个元素插入时,必须指定数组容量大小、初始化数组以容纳元素。

查看添加方法

// 直接添加元素
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}
// 在指定的位置加上元素
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++;
}

首先看到的是带三个感叹号的注释 // Increments modCount!!,这个 modCount 是干什么呢的?

  • 点进去发现是继承自 AbstractList

    protected transient int modCount = 0;
    

    这个属性就是:已对该列表进行结构化的修改次数,否则的话会干扰正在迭代的,产生错误的结果。如果这个 field 变换不正常的话,就会在听下一个迭代(next,remove,previous,set,add 操作的时候)产生在并发里面很常见的并发修改异常(这就是根源。Man,I Learned so much.)ConcurrentModificationException

    面对迭代过程中的并发修改,这提供了 快速故障 fast-fail 行为,而不是不确定的行为。

    如果子类希望提供快速失败的迭代器(和列表迭代器),则只需在其 add(int,E)和 remove(int)方法(以及任何其他覆盖该方法导致结构化的方法)中递增此字段即可。 在添加或者移出的时候,这个加不能超过一个。否则会抛出虚假的 ConcurrentModificationException

接着看

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 计算容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 如果是第一次添加数据,是无参构造第一次添加数据
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 获取到 10 和 size+1
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}


private void ensureExplicitCapacity(int minCapacity) {
    // 避免并发修改异常
    modCount++;

    if (minCapacity - elementData.length> 0)
        // 进行扩容
        grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    // 原来 * 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);
}
// 可以扩容的最大容量,当想要扩容的容量大于 MAX_ARRAY_SIZE 时调用
private static int hugeCapacity(int minCapacity) {
    // 是否扩容的时候变成了负数
    if (minCapacity < 0) // overflow 内存不足
        throw new OutOfMemoryError();
    // 返回
    return (minCapacity> MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
}

总结:

  1. 有 index 添加会进行 index range check, 没有 index 就进行保证容量的的方法
  2. 先计算容量判断是否是默认容量 10,就返回 Math.max(DEFAULT_CAPACITY, index); 或者 index
  3. 拿到上面的值 a, 先加 modCount++,如果a大于存储元素的长度的话就进行 grow 操作
  4. 加入需要 grow,传入最大值
    1. 拿到原来数组的长度 * 1.5,用位运算
    2. 如过 *1.5 后,还小于 a,就返回 a
    3. 如果大于 2^31-8,就进行 hugeCapacity 操作(判断是否溢出,就抛出来内存不足异常,否则就返回,MAX_ARRAY_SIZE),否则就调用Arrays.copyOf方法进行拷贝。

什么时候扩容?

  1. 主动扩容
  2. 添加时候扩容 从 public void ensureCapacity(int minCapacity) 可以看出,我们可以主动调用该方法进行扩容。

此外,在调用 add/addAll 等添加元素的方法时,ArrayList 也会调用内部扩容方法(ensureCapacityInternal)来主动扩容

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

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

数组底层的方法

扩容的时候其实调用了 Arrays.copyOf 方法,最后也是调用了 System.arraycopy

// Arrays.copyOf
public static <T> T[] copyOf(T[] original, int newLength) {
    return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
    @SuppressWarnings("unchecked")
    T[] copy = ((Object)newType == (Object)Object[].class)
        ? (T[]) new Object[newLength]
        : (T[]) Array.newInstance(newType.getComponentType(), newLength);
    System.arraycopy(original, 0, copy, 0,
                        Math.min(original.length, newLength));
    return copy;
}

public native Class<?> getComponentType();

//  System中的 copy :native 方法
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

其他几个 public 方法

  • clear() fori将每个元素设置为 null,让 gc 做工作。
  • clone() 调用父类的 clone 方法,生成新ArrayList,然后把数据 copy 过去(Arrays.copy),modCount=0
  • toArray() Arrays.copy 返回新拷贝的数据。

总结

  • 基于数组实现

  • 可以动态扩容,每次*1.5 倍的长度增加

  • 元素可以为 null

  • 不是并发安全的,是通过modCount来进行判断的

  • 如果想并发安全:

    1. Vector 不行,效率低
    2. 使用 Collections.synchronizedList()
    3. JUC 中的并发容器 new CopyOnWriteArrayList<>();
  • 为什么数组中的最大元素是 Integer.MAX_VALUE - 8

    • stackoverflow

      数组对象(例如int值的数组)的形状和结构类似于标准Java对象的形状和结构。主要区别在于,数组对象还有一条额外的元数据,它表示数组的大小。然后,数组对象的元数据包括:Class:指向类信息的指针,该信息描述对象的类型。对于int字段数组,这是指向int []类的指针。 标志:描述对象状态的标志的集合,包括对象的哈希码(如果有的话)以及对象的形状(即,对象是否为数组)。 Lock:对象的同步信息,即对象当前是否同步。 Size:数组的大小。

    • From Java code to Java heap