参考资料:JDK 8 中 ArrayList 的自动扩容 要详细了解序列化,可以参考该博客:Java 中的 transient 关键字

先说结论

JDK 16 中的 ArrayList 自动扩容 - 图1


源码解读

我们知道数组本身是无法直接扩容的,要想存储更多的数据需要新建一个数组,再把之前数组的所有内容填充进去。而 ArrayList 的自动扩容机制,使得其在处理变长度数组的时候非常方便。

字段

在了解扩容机制之前,先对 ArrayList 中的各个字段进行解读:

  1. // 用于支持 Serializable 机制,与扩容无关
  2. private static final long serialVersionUID = 8683452581122892189L;
  3. // 默认的扩容量的长度为10
  4. private static final int DEFAULT_CAPACITY = 10;
  5. // 在类加载时就定义的两个空元素数组
  6. private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
  7. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
  8. // ArrayList 中元素存储的底层实现,本质是一个Object数组
  9. transient Object[] elementData;
  10. // 存储当前 ArrayList 中元素的数量
  11. private int size;

可以看到,本质上 ArrayList 底层用于存储元素的字段是一个 Object 数组,这个数组使用了 transient 关键字修饰,直译是“暂时的,临时的”,在 Java 中的含义是不可序列化的,即在程序运行过程中该变量只会存在于调用者的内存中,而不会写入磁盘。

构造器

接下来查看 ArrayList 是怎么初始化的, 在 ArrayList 类中给出了如下三个构造器:

  1. public ArrayList(int initialCapacity) {
  2. if (initialCapacity > 0) {
  3. this.elementData = new Object[initialCapacity];
  4. } else {
  5. if (initialCapacity != 0) {
  6. throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
  7. }
  8. this.elementData = EMPTY_ELEMENTDATA;
  9. }
  10. }
  11. public ArrayList() {
  12. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  13. }
  14. public ArrayList(Collection<? extends E> c) {
  15. Object[] a = c.toArray();
  16. if ((this.size = a.length) != 0) {
  17. if (c.getClass() == ArrayList.class) {
  18. this.elementData = a;
  19. } else {
  20. this.elementData = Arrays.copyOf(a, this.size, Object[].class);
  21. }
  22. } else {
  23. this.elementData = EMPTY_ELEMENTDATA;
  24. }
  25. }

可以看到,第一个和第三个构造器调用的时候,会按照所传参数初始化 elementData 数组,数组大小与所传参数有关。当传入的参数小于等于0(调用第一个构造器)或传入了一个空的 Collection 对象(调用第三个构造器)时,elementData 数组会被初始化为 EMPTY_ELEMENTDATA 数组,它是一个空的 Object 数组对象。而在调用默认构造器的时候,elementData 数组会被初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,它也是一个空的 Object 数组对象。

add()

当我们向 ArrayList 对象添加元素的时候,可能会发生 elementData 数组原本的长度不足,这时候就应该触发自动扩容机制,所以接下来查看 add() 方法是如何处理的。

  1. private void add(E e, Object[] elementData, int s) {
  2. // 触发自动扩容机制
  3. if (s == elementData.length) {
  4. elementData = this.grow();
  5. }
  6. elementData[s] = e;
  7. this.size = s + 1;
  8. }
  9. public boolean add(E e) {
  10. ++this.modCount;
  11. this.add(e, this.elementData, this.size);
  12. return true;
  13. }
  14. public void add(int index, E element) {
  15. this.rangeCheckForAdd(index);
  16. ++this.modCount;
  17. int s;
  18. Object[] elementData;
  19. // 触发自动扩容机制
  20. if ((s = this.size) == (elementData = this.elementData).length) {
  21. elementData = this.grow();
  22. }
  23. System.arraycopy(elementData, index, elementData, index + 1, s - index);
  24. elementData[index] = element;
  25. this.size = s + 1;
  26. }

可以看到,如果直接插入某一元素,会在内部调用私有的 add() 方法,该方法会将 size 字段的值和 elementData 数组的大小进行比较,当二者相等,也即 elementData 数组存满了的时候,就会调用 grow() 方法。同样地,如果在某一个索引处添加某个元素,也会触发这一机制。

grow()

接下来考察 grow() 方法是如何实现的,源码如下所示:

  1. private Object[] grow(int minCapacity) {
  2. int oldCapacity = this.elementData.length;
  3. if (oldCapacity <= 0 && this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
  4. return this.elementData = new Object[Math.max(10, minCapacity)];
  5. } else {
  6. int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);
  7. return this.elementData = Arrays.copyOf(this.elementData, newCapacity);
  8. }
  9. }
  10. private Object[] grow() {
  11. return this.grow(this.size + 1);
  12. }

当外部调用无参的 grow() 方法的时候,该方法会往有参的 grow() 方法中传入一个参数,该参数为目前 ArrayList 中实际存储的元素数量 +1,表示本次扩容所需的最小容量。因此可以想见,对于 addAll() 这样的一次插入多个元素的方法而言,其会直接调用有参的 grow() 方法,实际也正是如此。

  1. public boolean addAll(Collection<? extends E> c) {
  2. Object[] a = c.toArray();
  3. ++this.modCount;
  4. int numNew = a.length;
  5. if (numNew == 0) {
  6. return false;
  7. } else {
  8. Object[] elementData;
  9. int s;
  10. // 触发扩容机制
  11. if (numNew > (elementData = this.elementData).length - (s = this.size)) {
  12. elementData = this.grow(s + numNew);
  13. }
  14. System.arraycopy(a, 0, elementData, s, numNew);
  15. this.size = s + numNew;
  16. return true;
  17. }
  18. }
  19. public boolean addAll(int index, Collection<? extends E> c) {
  20. this.rangeCheckForAdd(index);
  21. Object[] a = c.toArray();
  22. ++this.modCount;
  23. int numNew = a.length;
  24. if (numNew == 0) {
  25. return false;
  26. } else {
  27. Object[] elementData;
  28. int s;
  29. // 触发扩容机制
  30. if (numNew > (elementData = this.elementData).length - (s = this.size)) {
  31. elementData = this.grow(s + numNew);
  32. }
  33. int numMoved = s - index;
  34. if (numMoved > 0) {
  35. System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
  36. }
  37. System.arraycopy(a, 0, elementData, index, numNew);
  38. this.size = s + numNew;
  39. return true;
  40. }
  41. }

在有参的 grow() 方法中,先读取扩容之前的 elementData 数组的长度,如果该数组之前长度就为0并且就是字段中对应的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组(即调用的是无参构造器),就将 elementData 数组扩容到需求的最小长度,如果这个最小长度比10小,那就会直接扩容到10;而如果该数组之前长度不为0,或者初始化的时候调用的不是无参构造器,那么就会调用 ArraySupports 类中的 newLength 方法来获得需要增长的长度,并返回扩容后新的 elementData 数组。newLength 方法是这样定义的:

  1. // 调用语句
  2. ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);
  3. public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
  4. int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
  5. return newLength - 2147483639 <= 0 ? newLength : hugeLength(oldLength, minGrowth);
  6. }

该方法有三个参数,第一个参数代表之前容量,第二个参数代表最少应该增加的容量,第三个参数代表优先考虑增加的容量,最终增加的容量大小会根据后两者中的较大值来确定,最后还会添加一个判断来保证返回值不会溢出。

而 grow() 方法中传入的第三个参数为过去的值按位右移1位,可以理解为整型计算中的除以2。至此我们可以得到整个自动扩容的流程,将其整理为流程图为:
JDK 16 中的 ArrayList 自动扩容 - 图2