📦 1. 初始容量与触发条件
-
初始容量:当使用无参构造函数创建
ArrayList
时,其默认初始容量为 10(在 JDK 7 及之后的版本中,初始数组通常是一个空引用,直到第一次添加元素时才会分配一个长度为10的数组)。你也可以通过带参数的构造函数(如new ArrayList<>(20)
)来指定一个初始容量,这有助于在预先知道元素大致数量时避免初期的频繁扩容。 -
触发扩容的条件:每当调用
add()
或addAll()
方法添加新元素时,ArrayList
会检查当前元素的数量(size
)加1是否超过了底层数组的长度(elementData.length
)。如果size + 1 > elementData.length
,则会触发扩容机制。
⚙️ 2. 扩容的核心步骤
扩容过程主要通过 grow()
方法实现,其核心步骤和策略如下:
-
计算新容量:
- •
新容量的计算通常遵循 1.5倍 的原则(即
newCapacity = oldCapacity + (oldCapacity >> 1)
),这里的位运算oldCapacity >> 1
等价于除以2。 - •
例如,若当前容量为10,则扩容后新容量为
10 + (10 >> 1) = 15
。 - •
这是一种在内存空间利用和扩容操作频率之间取得的平衡。倍数过大(如2倍)可能导致空间浪费,倍数过小(如1.2倍)则可能导致扩容操作过于频繁。
- •
-
确保最小容量:
- •
如果按1.5倍计算出的新容量仍然小于实际需要的最小容量(
minCapacity
,通常是当前元素数量size
加上本次新增的元素数量),则会直接采用minCapacity
作为新容量。这一点在使用addAll()
一次性添加多个元素时尤为重要。
- •
-
处理大容量情况:
- •
ArrayList
内部定义了一个最大数组容量限制MAX_ARRAY_SIZE
(通常为Integer.MAX_VALUE - 8
)。 - •
如果计算出的新容量甚至超过了
MAX_ARRAY_SIZE
,则会根据minCapacity
的值进一步判断:若minCapacity
大于MAX_ARRAY_SIZE
,则新容量设置为Integer.MAX_VALUE
;否则设置为MAX_ARRAY_SIZE
。如果所需容量极大,可能会抛出OutOfMemoryError
。
- •
-
创建新数组并复制数据:
- •
确定新容量后,会使用
Arrays.copyOf()
方法创建一个新的、容量更大的数组,并将原有数组中的所有元素复制到新数组中。 - •
最后,将
ArrayList
内部的数组引用elementData
指向这个新数组。 - •
这个复制操作的时间复杂度是 O(n),其中 n 是原数组中的元素数量,这是扩容操作的主要性能开销来源。
- •
⚡️ 3. 扩容的性能影响与优化
-
性能影响:
-
时间开销:每次扩容都涉及旧数组数据的复制,这是一个相对耗时的操作。频繁扩容会影响程序性能。
-
空间开销:扩容后,新数组的容量是旧的1.5倍,可能会存在一些未使用的空间,造成一定的内存浪费。
-
-
优化建议:
-
预估容量并指定初始容量:如果你能大致预估
ArrayList
最终会存储的元素数量,最好在创建时通过构造函数指定一个足够的初始容量(例如new ArrayList<>(1000)
)。这是避免多次不必要的扩容、提升性能最有效的方法。 -
使用
ensureCapacity()
方法手动提前扩容:如果你已经创建了一个ArrayList
并且后续需要大量添加元素(例如通过一个循环),可以提前调用ensureCapacity(int minCapacity)
方法,一次性将容量至少扩展到指定的minCapacity
。这可以避免在添加元素过程中多次触发自动扩容。 -
在元素添加完成后使用
trimToSize()
:如果你非常在意内存的使用,并且确定后续不会再添加大量新元素,可以调用trimToSize()
方法。这个方法会将底层数组的容量裁剪至当前实际元素的个数,释放多余的空间。但请注意,此操作本身也可能涉及数组复制。
-