目录
属性
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// 序列化版本号
@java.io.Serial
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组,用于空实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组,用于默认大小的空实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储ArrayList元素的数组
transient Object[] elementData; // non-private to simplify nested class access
// ArrayList的大小(包含的元素个数)
private int size;
}
ArrayList源码中定义的默认容量DEFAULT_CAPACITY等于10,定义了两个空数组,以及一个真正存储元素的Object数组。
构造方法
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
空参构造方法会将默认数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给真正存储对象的数组elementData,在后续添加元素的时候再使用到默认容量,将数组大小扩容到10。
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);
}
}
传入需要构建的ArrayList的初始大小,传入的initialCapacity如果等于0,则将属性中的空数组EMPTY_ELEMENTDATA赋值给真正存储对象的数组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;
}
}
构造包含指定集合元素的列表
ArrayList的扩容机制
jdk8的扩容机制
// 添加元素到末尾
public boolean add(E e) {
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
// 在指定位置插入元素
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1);
// 将index及其之后的元素后移一位
System.arraycopy(elementData, index, elementData, index + 1, size - index);
elementData[index] = element;
size++;
}
在判断了扩容之后,就用System.arraycopy()进行原地数组的复制,将index开始的元素往后移一个位置,最后给数组的index的位置赋值。
可能有些读者没见过System.arraycopy()这个方法,此方法是一个本地方法,我们常用的Arrays.copyOf()方法底层其实就是调用了这个方法,如下图中。
既然Arrays.copyOf()本质上调用的是System.arraycopy()方法,那么效率上Arrays.copyOf()肯定能是不及System.arraycopy()的。在这里粘一下源码,大家可以阅读一下。
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;
}
接下来讲解一下jdk8中的扩容机制
ensureCapacityInternal(int minCapacity) 方法
private void ensureCapacityInternal(int minCapacity) {
// 检查当前数组是否是默认空数组
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果是,确保最小容量至少为默认容量
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 确保显式容量
ensureExplicitCapacity(minCapacity);
}
确保 ArrayList 的容量足够以容纳至少 minCapacity 个元素,minCapacity 表示需要的最小容量。
如果当前数组是默认空数组(即未初始化),则将 minCapacity 设置为默认容量(10)和传入的 minCapacity 中的较大值,调用 ensureExplicitCapacity 方法来检查并扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 增加修改计数,确保在迭代时可以检测到并发修改
// 检查当前容量是否小于所需的最小容量
if (minCapacity - elementData.length > 0)
grow(minCapacity); // 如果小于,调用 grow 方法进行扩容
}
增加 modCount,用于检测并发修改。如果当前数组的长度小于 minCapacity,则调用 grow 方法进行扩容。
grow 方法
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 获取当前数组的容量
// 计算新的容量,通常为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新的容量小于所需的最小容量,则使用最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新的容量超过最大数组大小,则调用 hugeCapacity 方法处理
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 扩容并返回新数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
获取当前数组的容量 oldCapacity,计算新的容量 newCapacity,通常为旧容量的1.5倍(oldCapacity + (oldCapacity >> 1))。如果 newCapacity 小于 minCapacity,则将 newCapacity 设置为 minCapacity,如果 newCapacity 超过最大数组大小(MAX_ARRAY_SIZE),则调用 hugeCapacity 方法处理,最后使用 Arrays.copyOf 方法扩容并返回新数组。
hugeCapacity(int minCapacity) 方法
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // 检查是否溢出
throw new OutOfMemoryError(); // 如果溢出,抛出内存不足错误
// 返回最大数组大小或 Integer.MAX_VALUE
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE : // 如果需要的容量超过最大数组大小,返回 Integer.MAX_VALUE
MAX_ARRAY_SIZE; // 否则返回最大数组大小
}
检查 minCapacity 是否小于0(即是否发生溢出),如果是,则抛出 OutOfMemoryError,如果 minCapacity 超过 MAX_ARRAY_SIZE,则返回 Integer.MAX_VALUE,表示无法创建更大的数组,否则,返回 MAX_ARRAY_SIZE。
jdk17的扩容机制
在jdk17中有三个add方法,先介绍其中一个
//公有方法
public void add(int index, E element) {
//对传入的index进行判断,若是非法的数据则抛出异常
rangeCheckForAdd(index);
//用于fail-fast(快速失败)机制
modCount++;
final int s;
Object[] elementData;
//
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
//进行数组的复制
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
add(int index, E element)该方法会在指定的数组位置上添加元素。如代码注释如上,modCount值是用于快速失败机制的,在后文会进行进一步讲解。在代码中会拿ArrayList中的size和元素数组elementData的大小进行比较,若是相等则说明现在已经到边界,接下来就需要调用grow()方法进行扩容。
在判断了扩容之后,就用System.arraycopy()进行原地数组的复制,将index开始的元素往后移一个位置,最后给数组的index的位置赋值。
可能有些读者没见过System.arraycopy()这个方法,此方法是一个本地方法,我们常用的Arrays.copyOf()方法底层其实就是调用了这个方法,如下图中。
既然Arrays.copyOf()本质上调用的是System.arraycopy()方法,那么效率上Arrays.copyOf()肯定能是不及System.arraycopy()的。在这里粘一下源码,大家可以阅读以下
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;
}
还有两个add方法是相互调用的,代码如下
//公有方法
public boolean add(E e) {
//用于fail-fast(快速失败)机制
modCount++;
//执行重载add方法,传入元素,elementData数组和size
add(e, elementData, size);
return true;
}
//私有方法
private void add(E e, Object[] elementData, int s) {
//判断数组是否需要扩容,若是需要扩容,就执行grow()方法
if (s == elementData.length)
elementData = grow();
//给传入的s位置赋值元素
elementData[s] = e;
//将元素的size改为s+1
size = s + 1;
}
代码注释如上,就是调用了重载的方法,判断是否需要扩容,最后在进行赋值,也不过多叙述了。
接下来就来讲解一下扩容的过程
private Object[] grow() {
return grow(size + 1); // 调用 grow 方法,传入当前大小 + 1,表示至少需要一个额外的空间
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length; // 获取当前数组的容量
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 如果当前容量大于0,或者不是默认空数组
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
// 计算新的容量,使用 ArraysSupport.newLength 方法
return elementData = Arrays.copyOf(elementData, newCapacity); // 扩容并返回新数组
} else {
// 如果是默认空数组,初始化为默认容量
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
代码的详细注释如上图,扩容的过程会调用ArraysSupport类的方法,记住这里分别传入的值为原elementData数组的大小,最小需要的扩容数,还有原elementData数组的大小左移一位,约为elementData数组的大小的0.5倍,elementData数组的大小是偶数就是0.5倍,否则会不足0.5倍,有精度丢失。
接下来讲解调用的ArraysSupport类的方法
//oldLength:当前数组的长度。
//minGrowth:最小需要增加的长度。
//prefGrowth:首选增长长度(通常是当前长度的一半)
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // 计算首选的新长度
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength; // 如果在合理范围内,返回首选长度
} else {
// 如果超出范围,调用 hugeLength 方法处理
return hugeLength(oldLength, minGrowth);
}
}
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth; // 计算最小需要的长度
if (minLength < 0) { // 检查是否溢出
throw new OutOfMemoryError(
"Required array length " + oldLength + " + " + minGrowth + " is too large");
} else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
return SOFT_MAX_ARRAY_LENGTH; // 如果在合理范围内,返回 SOFT_MAX_ARRAY_LENGTH
} else {
return minLength; // 否则返回计算的最小长度
}
}
代码的注解如上,需要说明的是,扩容的大小会取Math.max(minGrowth, prefGrowth)两者的最大值,以便扩容后的容量可以容纳添加的元素数量,一般add方法只添加一个元素,minGrowth=1,所以会扩容到1.5倍左右的大小,但是如果是addAll方法添加一系列元素,且添加元素后需要的大小大于1.5倍,那么就会扩容至刚好能容纳所添加元素的大小。
这样做不会造成频繁扩容,可以一次添加大量元素,也只会扩容一次。
JDK 17 的 ArrayList 扩容机制通过 grow 方法和 newLength 方法的组合,灵活地处理了不同情况下的扩容需求。
- 性能优化:通过计算首选长度和最小增长,避免了不必要的扩容,提升了性能。
- 安全性:通过溢出检查和合理范围限制,确保了内存的安全使用。
- 可维护性:将扩容逻辑分离到 ArraysSupport 类中,使得代码更清晰,易于维护。
这种设计使得 ArrayList 在处理动态数组时更加高效和安全,能够适应不同的使用场景。
ArrayList的JDK 8和JDK 17扩容机制的对比
JDK 8 和 JDK 17 的扩容机制并不是绝对的1.5倍扩容,两者都允许根据实际需要动态调整容量。JDK 17 的实现通过更灵活的扩容策略和更清晰的代码结构,提升了性能和可维护性,JDK 8扩容逻辑较为简单,分为几个步骤,JDK 17的逻辑更为集中,减少了代码的复杂性。
个人去测试过JDK 8 和 JDK 17 中ArrayList添加数据的一个表现情况,JDK17中ArrayList的效率大概会比JDK 8有5%左右的提升。