ArrayList 扩容

695 阅读4分钟

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

通过注释描述,可以大致了解到,ArrayList 具有自动扩容机制。扩容触发条件是:

  • 向 ArrayList 添加元素时
  • 调用ensureCapacity方法

下面时 ArrayList 添加元素的 add方法

 public boolean add(E e) {
     // size 是当前元素个数(需要与容量进行区分)
     ensureCapacityInternal(size + 1);  // Increments modCount!!
     elementData[size++] = e;
     return true;
 }

其中调用了ensureCapacityInternal方法,与ensureCapacity方法名类似,Internal 可以大致猜测是因为其为私有方法。


下面对ensureCapacityInternalensureCapacity进行简单的分析,其代码如下:

简单解释以下其中的属性:

  • transient Object[] elementData;

    ArrayList 中实际存储数据的数组的缓冲区,扩容操作也是操作该数组

  • private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    一个空数组

  • private static final int DEFAULT_CAPACITY = 10;

    默认的初始容量大小

 // minCapacity 为所需的最小容量
 public void ensureCapacity(int minCapacity) {
     
     //  验证 elementData 是否为默认创建的数组
     //  DEFAULTCAPACITY_EMPTY_ELEMENTDATA 与构造方法有关,可见附录
     //  无参构造的 ArrayList 默认扩展大小为 10,否则最小为 0
     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);
     }
 }
 ​
 // 若是无参构造的 ArrayList ,已有默认容量(为10)
 // 当所需扩容容量小于默认容量,则仍未默认容量
 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));
 }
 ​

发现两者都调用了ensureExplicitCapacity方法,此处未见扩容的操作,于是继续看ensureExplicitCapacity方法。

 private void ensureExplicitCapacity(int minCapacity) {
     modCount++;
 ​
     // overflow-conscious code
     // 判断是否需要执行扩容
     if (minCapacity - elementData.length > 0)
         grow(minCapacity);
 }

modCount用于记录 ArrayList 当前修改次数,对扩容影响不大。下面对扩容后的容量和当前的长度做对比,符合则执行 grow 方法进行扩容。

见下方代码,可以看出主要的扩容动作在grow方法中实现。

 private void grow(int minCapacity) {
     // overflow-conscious code
     int oldCapacity = elementData.length;
     
     // 下面的位运算扩容大小想当于 oldCapacity * 1.5:使用位操作提高效率
     // 对下面判断条件概括一下:
     //      默认扩容 1.5 倍
     //      若扩容 1.5 倍后 小于 所需扩容大小(minCapacity)则不容扩容 1.5 倍就够用,新容量为 minCapacity
     //      若超出最大容量,则进行一些处理,后面已经给出代码
     int newCapacity = oldCapacity + (oldCapacity >> 1);
     if (newCapacity - minCapacity < 0)
         newCapacity = minCapacity;
     if (newCapacity - MAX_ARRAY_SIZE > 0)
         newCapacity = hugeCapacity(minCapacity);
     // minCapacity is usually close to size, so this is a win:
     
     // 对数组进行扩容和数据迁移
     elementData = Arrays.copyOf(elementData, newCapacity);
 }
 private static int hugeCapacity(int minCapacity) {
     if (minCapacity < 0) // overflow
         throw new OutOfMemoryError();
     return (minCapacity > MAX_ARRAY_SIZE) ?
         Integer.MAX_VALUE :
         MAX_ARRAY_SIZE;
 }

总结

一方面,开发者可以通过ensureCapacity进行指定大小的扩容。

另一方面,是 ArrayList 内部的扩容,拿 add方法举例:

  1. 首先会调用扩容方法,因为插入元素,需要的容量大小为 size + 1
  2. ensureCapacityInternal方法中判断所需扩容大小(通过空数组判断是否存在默认容量)
  3. ensureExplicitCapacity中判断是否需要扩容
  4. grow中进行扩容操作

ArrayList 中元素数量和容量分离,同时扩容操作时会对扩容大小进行多方面的判断操作,具有下面的优点:

  • 元素数量 与 容量 分离:在尽量少触发扩容的情况下实现自动扩容
  • 扩容大小判断:保证扩容大小能够不占用大量空间
  • 采用 位运算:提高运算效率

附录

ArrayList 构造方法

ArrayList 构造方法如下三种:

  • 无参:创建默认容量大小的 ArrayList
  • (int initialCapacity):创建指定容量大小的 ArrayList
  • ArrayList(Collection<? extends E> c):根据传入的集合进行创建

若传入的集合长度为 0 或者指定容量为 0 进行创建,elemenData 是内部 private static final Object[] EMPTY_ELEMENTDATA = {};引用的空数组。

若无参构造,elemenData 是内部 DEFAULTCAPACITY_EMPTY_ELEMENTDATA引用的空数组。

两个空数组:EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA

从注释上来看,其实主要是用于扩容时的区分,我的想法如下:

因为有参传入时,已经有实际的容量大小,如果为 0 ,此时 ArrayList 的容量也为 0 。

而使用无参传入时,虽然没有数据,但是默认容量是 10。

在上面的扩容分析中,也可以看到扩容判断用的为DEFAULTCAPACITY_EMPTY_ELEMENTDATA

因为无参构造时虽然没有数据,但已有默认容量,可以判断无参构造的 ArrayList 添加元素时是否触发扩容。

 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) {
     Object[] a = c.toArray();
     if ((size = a.length) != 0) {
         if (c.getClass() == ArrayList.class) {
             elementData = a;
         } else {
             elementData = Arrays.copyOf(a, size, Object[].class);
         }
     } else {
         // replace with empty array.
         elementData = EMPTY_ELEMENTDATA;
     }
 }

\