基本使用

ArrayList位于java.util包,在JDK1.2引入,ArrayList是List接口中较为常用的类型,基于数组实现,查询效率高,删除和指定位置的插入元素效率相对较低。查看其类图关系:

ArrayList.png

底层实现

查看ArrayList的源码,在该类中定义了如下的类变量或成员变量,这也说明其底层的数据结构实现为Object数组,允许添加null元素。

  1. // 静态变量
  2. private static final int DEFAULT_CAPACITY = 10;
  3. private static final Object[] EMPTY_ELEMENTDATA = {};
  4. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  5. // 成员变量
  6. transient Object[] elementData;
  7. private int size;

关于在ArrayList中定义的这些变量,DEFAULT_CAPACITY和size含义比较简单,一个是初始长度大小,一个是当前集合长度大小:

  • DEFAULT_CAPACITY:默认初始化长度为10
  • size:ArrayList实例中包含的元素个数。

以下三个变量需要注意的是,包含了两个已经初始化的空数组常量,和一个用关键字transient修饰的未初始化的成员变量:

  • elementData:存储ArrayList元素的缓冲区,在进行元素操作的时候,该变量所指向的空间即实际数据存储区域,由于使用transient关键字修饰也就意味着不可序列化,其生命周期仅存于调用者的内存中而不会持久化到磁盘。当添加第一个元素时,任何具有elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList都将扩展为长度为DEFAULT_CAPACITY的Object数组。
  • EMPTY_ELEMENTDATA:用于空实例的共享空数组实例。当调用ArrayList的有参构造时,如果指定的集合长度为0时,elementData会指向EMPTY_ELEMENTDATA。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:用于默认大小的空实例的共享空数组实例。当调用ArrayList无参构造时,elementData会指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA。用来确定添加第一个元素时需要膨胀的长度。

    扩容机制

    在JDK1.8的源码中,ArrayList做了一些优化,在创建一个空数组实例时并不会立即对其长度进行初始化,在使用时才会判断容量,进行扩容。
    下面从调用ArrayList的无参构造,并添加一个元素,对源码实现进行分析,查看下面测试代码

    1. public class TestArrayList {
    2. public static void main(String[] args) {
    3. List<Integer> list = new ArrayList<>();
    4. list.add(1);
    5. System.out.println(list);
    6. }
    7. }

    此处使用了ArrayList的无参构造,查看源码,elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA,此时数组长度为0。

    1. public ArrayList() {
    2. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    3. }

    当调用add方法时,会进行如下操作,查看源码,首先会调用ensureCapacityInternal方法进行容量校验。

    1. public boolean add(E e) {
    2. // 校验数组容量
    3. ensureCapacityInternal(size + 1); // Increments modCount!!
    4. // 指定下标位置,在Object数组elementData的缓冲区中存储元素
    5. elementData[size++] = e;
    6. return true;
    7. }

    查看ensureCapacityInternal方法中的实现,这里面进一步进行显式容量的确认,并且使用calculateCapacity方法对当前ArrayList对象的elementData进行计算,返回结果。

    1. private void ensureCapacityInternal(int minCapacity) {
    2. ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    3. }

    查看calculateCapacity方法的源码,首次添加元素并且ArrayList的初始化长度为0时,对ArrayList的长度进行初始化,通过计算返回默认值DEFAULT_CAPACITY。这里也说明,如果ArrayList通过在new的时候指定了长度,且其值大于默认值,则返回elementData的当前长度。

    1. private static int calculateCapacity(Object[] elementData, int minCapacity) {
    2. // 在首次实例化ArrayList时,elementData指向DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    3. // 因此在第一次添加元素时,if条件满足。此时DEFAULT_CAPACITY为10,minCapacity值为1
    4. // 所以第一次计算结果返回为此时DEFAULT_CAPACITY的默认值10
    5. if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    6. return Math.max(DEFAULT_CAPACITY, minCapacity);
    7. }
    8. return minCapacity;
    9. }

    继续查看方法ensureExplicitCapacity源码,此时方法入参minCapacity的值为DEFAULT_CAPACITY=10

    1. private void ensureExplicitCapacity(int minCapacity) {
    2. // 继承自Abstract类的保护参数,用来记录集合被修改的次数,保证迭代不被影响,这里不再扩展
    3. modCount++;
    4. // 此时minCapacity=10,elementData.length=0,因此会对ArrayList进行扩容
    5. if (minCapacity - elementData.length > 0)
    6. grow(minCapacity);
    7. }

    查看grow方法的源码,minCapacity的值为size+1

    1. private void grow(int minCapacity) {
    2. // 首次调用oldCapacity为0
    3. int oldCapacity = elementData.length;
    4. // 此时newCapacity的值为0 + 0 >> 1 = 0
    5. int newCapacity = oldCapacity + (oldCapacity >> 1);
    6. // 满足0 - 10 < 0
    7. if (newCapacity - minCapacity < 0)
    8. // 此时newCapacity = 10
    9. newCapacity = minCapacity;
    10. // 如果扩容后的大小超过了MAX_ARRAY_SIZE,会对容量进行重新计算
    11. if (newCapacity - MAX_ARRAY_SIZE > 0)
    12. newCapacity = hugeCapacity(minCapacity);
    13. // minCapacity is usually close to size, so this is a win:
    14. elementData = Arrays.copyOf(elementData, newCapacity);
    15. }
  • MAX_ARRAY_SIZE:这也是要ArrayList的最大大小。MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,至于为什么要减8,源码中对此做了解释:一些 VM 在数组中保留一些header words,尝试分配更大的数组可能会导致 OutOfMemoryError,请求的数组大小超过VM限制。

查看hugeCapacity方法

  1. private static int hugeCapacity(int minCapacity) {
  2. // 如果size+1后的结果minCapacity溢出了,会抛出内存溢出异常
  3. if (minCapacity < 0) // overflow
  4. throw new OutOfMemoryError();
  5. // 如果扩容后的值minCapacity大于Integer.MAX_VALUE - 8,那就取Integer.MAX_VALUE
  6. // 如果下一次再进行递增,就会抛出OutOfMemoryError异常,其实这里也说明了ArrayList的底层
  7. // 数组最大长度为MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
  8. return (minCapacity > MAX_ARRAY_SIZE) ?
  9. Integer.MAX_VALUE :
  10. MAX_ARRAY_SIZE;
  11. }

进行newCapacity的值确定后,会调用Arrays.copyOf进行数据复制,查看copyOf方法的源码

  1. // 该方法的入参为原始数组引用,以及newCapacity的值
  2. public static <T> T[] copyOf(T[] original, int newLength) {
  3. return (T[]) copyOf(original, newLength, original.getClass());
  4. }

数组的copy过程调用了工具类Arrays中的方法,调用arraycopy方法进行数据复制完成后,对于需要扩容的长度会使用null来占位填充。

  1. public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
  2. @SuppressWarnings("unchecked")
  3. // 进行泛型类型判断,如果不能确定类型使用Object表示
  4. T[] copy = ((Object)newType == (Object)Object[].class)
  5. ? (T[]) new Object[newLength]
  6. : (T[]) Array.newInstance(newType.getComponentType(), newLength);
  7. System.arraycopy(original, 0, copy, 0,
  8. Math.min(original.length, newLength));
  9. return copy;
  10. }

然后调用native方法

  1. public static native void arraycopy(Object src, int srcPos,
  2. Object dest, int destPos,
  3. int length);

最终回到add方法,将新元素占用到size + 1下标位置,一个元素添加完成。如果使用add指定index进行元素添加,需要对其后面的元素进行时间复杂度O(n)的操作,直接add的时间复杂度为O(1),

  1. public boolean add(E e) {
  2. // 每次进行add时都要重复一遍对内部容量的确保校验动作
  3. ensureCapacityInternal(size + 1); // Increments modCount!!
  4. // 指定下标位置,在Object数组elementData的缓冲区中存储元素
  5. elementData[size++] = e;
  6. return true;
  7. }

总结

  • ArrayList的底层实现使用的是Object[]来存储数据,会根据当前对象的容量和要添加的元素进行自动扩容,通过调用操作系统层面的arraycopy方法进行数据拷贝。
  • 在JDK1.8中,调用无参构造创建ArrayList对象时并不会立即初始化空间,在首次调用add方法时才会初始化容量,自动扩容的长度为newCapacity = oldCapacity + (oldCapacity >> 1),也就是原容量的1.5倍。
  • ArrayList的理论最大长度为MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,一些 VM 在数组中保留一些header words,尝试分配更大的数组可能会导致 OutOfMemoryError,请求的数组大小超过VM限制。