在 Java 编程中,ArrayList是一个常用的集合类,它实现了List接口,底层基于数组实现。与普通定长数组不同,ArrayList能够根据元素的添加情况动态调整数组的大小,这就是其扩容机制。下面我们将深入剖析ArrayList扩容机制的源码。
有的同学可能在别的地方听过一些ArrayList扩容,大部分会说ArrayList底层是数组结构的,默认长度为10,当数组加满后会自动扩容1.5倍。这样的说法不完全对。
正确的步骤应该是如下的:
一、ArrayList
的构造方法
1. 无参构造
我们可以自己实际的去探索一下,打开idea,按快捷键ctrl+n,输入ArrayList,选择All Places,点击Java.util那个。
进入之后可以按ctrl+f12,按add,查找add方法,可以发现有很多add方法。
或者按Alt+7将整个大纲罗列出来,这里我们主要看的是空参构造
在这选中elementDate,按住ctrl+B,发现它是一个数组。
我们再看看DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个是什么,就会发现它是一个长度为0的数组,所以空参构造默认初始值为0。
当使用new ArrayList<>()
创建对象时,底层会创建一个空数组。相关源码如下:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里的DEFAULTCAPACITY_EMPTY_ELEMENTDATA
是一个空的Object
数组,ArrayList
将其赋值给用于存储元素的elementData
数组。
这里我将所有代码类全部汇总到一起,可以更好的理解过程。
2. 带初始容量的构造
当使用new ArrayList<>(int size)
时,如果传入的size
大于 0,会创建一个指定长度的数组;如果size
等于 0,会创建一个空数组,与无参构造的情况相同;如果size
小于 0,则会抛出IllegalArgumentException
异常。源码如下:
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);}
}
其中EMPTY_ELEMENTDATA
也是一个空数组,与DEFAULTCAPACITY_EMPTY_ELEMENTDATA
有所区别,主要用于标识通过带容量参数且容量为 0 的构造方法创建的情况。
3. 带集合参数的构造
当使用new ArrayList<>(Collection<? extends E> c)
时,会先将传入的集合转换为数组,然后判断数组长度:如果长度为 0,按无参构造方式创建空数组;如果不为 0,再判断传入的集合是否是ArrayList
类型。如果是,直接将转换后的数组赋值给elementData
;如果不是,使用Arrays.copyOf
方法进行二次复制,以确保安全性和隔离性。相关源码如下:
public ArrayList(Collection<? extends E> c) {elementData = c.toArray();if ((size = elementData.length) != 0) {if (c.getClass() == ArrayList.class) {this.elementData = elementData;} else {this.elementData = Arrays.copyOf(elementData, size, Object[].class);}} else {this.elementData = EMPTY_ELEMENTDATA;}
}
二、添加元素与扩容触发
当调用add(E e)
方法向ArrayList
中添加元素时,可能会触发扩容机制。add
方法的源码如下:
public boolean add(E e) {ensureCapacityInternal(size + 1); // 确保有足够的容量来存储新元素elementData[size++] = e;return true;
}
其中ensureCapacityInternal(int minCapacity)
方法用于确保内部数组有足够的容量来存储新元素。minCapacity
表示所需的最小容量,这里是当前元素个数size
加 1。
进入ensureCapacityInternal
方法:
private void ensureCapacityInternal(int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}ensureExplicitCapacity(minCapacity);
}
在这个方法中,如果elementData
是默认的空数组(即通过无参构造创建的情况),会将minCapacity
设置为默认容量DEFAULT_CAPACITY
(值为 10)和minCapacity
中的较大值。然后调用ensureExplicitCapacity
方法来判断是否需要扩容。
ensureExplicitCapacity
方法的源码如下:
private void ensureExplicitCapacity(int minCapacity) {modCount++;if (minCapacity - elementData.length > 0)grow(minCapacity);
}
这里通过比较所需的最小容量minCapacity
和当前数组elementData
的长度,如果minCapacity
大于elementData.length
,则说明当前数组容量不足,需要调用grow
方法进行扩容。
三、扩容的核心方法grow
grow
方法是ArrayList
扩容的关键,其源码如下:
private void grow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容为原来的1.5倍if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity);
}
在grow
方法中,首先获取当前数组的容量oldCapacity
,然后通过oldCapacity + (oldCapacity >> 1)
计算出一个新的容量newCapacity
,这里oldCapacity >> 1
表示将oldCapacity
右移一位,相当于除以 2,所以新容量是原来的 1.5 倍。
接着会进行两次判断:
- 如果
newCapacity
小于minCapacity
,说明 1.5 倍扩容后的容量仍不满足需求,此时将newCapacity
设置为minCapacity
,即按需扩容。 - 如果
newCapacity
超过了ArrayList
所能支持的最大数组大小MAX_ARRAY_SIZE
(在Integer.MAX_VALUE - 8
和Integer.MAX_VALUE
之间的一个值,具体取决于 JVM 的实现),则调用hugeCapacity
方法来确定最终的容量。
最后,通过Arrays.copyOf
方法将原数组的元素复制到新的、更大容量的数组中,完成扩容操作。
ArrayList
的扩容机制通过巧妙的设计和源码实现,在保证能够动态存储元素的同时,尽量减少不必要的数组复制操作,提高了性能。深入理解其扩容机制,有助于我们在使用ArrayList
时更好地进行性能优化和资源管理。