在Java中,ArrayList是一个动态数组,它允许在运行时改变其大小。当向ArrayList添加元素并且当前数组容量不足以容纳新元素时,ArrayList会自动扩容。
扩容原理
初始容量:当创建一个新的ArrayList时,可以指定一个初始容量。如果没有指定,则默认容量为10。
添加元素:当向ArrayList添加元素时,会检查当前数组的容量是否足够。如果足够,则直接在数组的末尾添加元素。
容量不足:如果当前数组的容量不足以容纳新元素,则需要进行扩容。扩容的逻辑是创建一个新的数组,其容量大约是原数组容量的1.5倍(具体实现可能因Java版本而异,但大致是这个比例)。然后,将原数组的所有元素复制到新数组中,并释放原数组的内存。最后,将新数组引用赋值给ArrayList的数组引用。
继续添加:扩容完成后,就可以在新数组的末尾添加新元素了。
例如:
import java.util.ArrayList; public class ArrayListExpansionExample { public static void main(String[] args) { // 创建一个初始容量为3的ArrayList ArrayList<Integer> list = new ArrayList<>(3); // 向ArrayList中添加元素 for (int i = 0; i < 5; i++) { list.add(i); System.out.println("After adding element " + i + ": " + list); System.out.println("Capacity: " + getArrayListCapacity(list)); } } private static int getArrayListCapacity(ArrayList<?> list) { try { java.lang.reflect.Field elementDataField = list.getClass().getDeclaredField("elementData"); elementDataField.setAccessible(true); Object[] elementData = (Object[]) elementDataField.get(list); return elementData.length; } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException(e); } }
}
运行这段代码,你将看到以下输出
After adding element 0: [0]
Capacity: 3
After adding element 1: [0, 1]
Capacity: 3
After adding element 2: [0, 1, 2]
Capacity: 3
After adding element 3: [0, 1, 2, 3]
Capacity: 4 (或可能是更大的值,取决于具体的实现和Java版本)
After adding element 4: [0, 1, 2, 3, 4]
Capacity: 6 (或更大的值)
ArrayList扩容过程中内存的变化:
假设我们有一个初始容量为4的ArrayList,并且已经存储了3个元素。现在我们要添加第4个元素。
1’初始状态:内存中有一个数组对象,其容量为3,并且存储了3个元素。ArrayList对象持有一个指向这个数组的引用。
2.添加第4个元素:当尝试添加第4个元素时,发现数组容量不足。
3.扩容:Java虚拟机(JVM)在堆内存中分配一个新的数组对象,其容量大约是原数组的1.5倍(这里假设是6)。然后,将原数组的所有元素复制到新数组中。此时,原数组对象变得不再被引用,因此可以被垃圾回收器回收。
4.更新引用:将ArrayList对象的数组引用更新为指向新的数组对象。
5.添加完成:现在可以在新数组的末尾添加第5个元素了。
需要注意的是,这个过程是透明的,对ArrayList的使用者来说是不可见的。我们只需要调用add方法添加元素,而不需要关心底层的扩容逻辑。
此外,频繁的扩容操作可能会导致性能下降,因为每次扩容都需要创建新的数组并复制元素。因此,如果知道要存储的元素数量,最好在创建ArrayList时指定一个合适的初始容量,以减少扩容的次数。