ArrayList 是我们最熟悉的 Java 集合之一,但你是否真正了解它在“幕后”是如何工作的?为什么说它“动态”?扩容机制到底是怎样的?
今天,我们将深入 ArrayList 的源码层面,一步步剖析它的创建、添加、扩容等核心机制。通过这篇解析,你将彻底明白 ArrayList 的“成长”奥秘。
ArrayList 的诞生:一个“空”数组的起点当我们执行 new ArrayList<>() 时,ArrayList 在底层做了什么?
ArrayList<String> list = new ArrayList<>();核心真相:此时,ArrayList 并没有立即创建一个长度为 10 的数组!相反,它非常“吝啬”,创建了一个长度为 0 的空数组。
这个空数组就是 ArrayList 的核心存储结构,其名字在源码中为 elementData。
// ArrayList 源码中的定义
transient Object[] elementData; // 非私有,以简化嵌套类访问同时,ArrayList 定义了一个关键的 int 变量:size。
size 变量的双重身份size 这个变量在 ArrayList 中扮演着至关重要的双重角色:
ArrayList 中实际存储了多少个元素。举个例子:当 size = 0 时,表示集合为空,下一个元素将被添加到索引 0 的位置。
当我们调用 list.add("Hello") 时,发生了什么?
add 方法首先会检查当前 elementData 数组的容量是否足够容纳新元素。由于 elementData 是一个长度为 0 的空数组,显然容量不足。ArrayList 会创建一个新的数组,其长度为 DEFAULT_CAPACITY(默认容量,值为 10)。"Hello" 被放入新数组的索引 0 位置。size 增长:size 变量从 0 增加到 1。此时,ArrayList 的内部结构如下:
elementData:指向一个长度为 10 的新数组。size:值为 1。随着我们不断添加元素,当 size 达到当前 elementData 数组的长度时(即数组“存满”),就迎来了常规扩容。
假设我们已经添加了 10 个元素,size 变为 10,数组已满。此时再添加第 11 个元素:
add 方法发现 size == elementData.length。ArrayList 会计算新数组的长度。这个长度是原数组长度的 1.5 倍。 10 + (10 >> 1) = 10 + 5 = 15elementData 指向这个新的、更大的数组。10 位置。size 更新:size 变为 11。这个过程会不断重复。当长度为 15 的数组再次装满时,下一次扩容的新长度将是 15 + (15 >> 1) = 15 + 7 = 22。
ArrayList 的扩容机制非常聪明,它能应对一次性添加大量元素的场景。
addAll)假设当前 ArrayList 已经有 10 个元素(size=10),数组长度也为 10(已满)。此时,我们调用 list.addAll(anotherList),而 anotherList 包含 100 个元素。
如果按照常规的 1.5 倍扩容,新数组长度为 15,但这 15 个位置显然无法容纳接下来要添加的 100 个元素。
ArrayList 如何应对?
ArrayList 会先计算出容纳所有新元素所需的最小容量。 size:1010 + 100 = 110ArrayList 会放弃 1.5 倍的规则,直接创建一个长度为 110 的新数组。anotherList 的 100 个元素全部复制到这个长度为 110 的新数组中。核心原则:ArrayList 的扩容策略是以实际需求为准。1.5 倍是常规情况下的优化增长,但当批量添加时,它会优先保证新数组的长度至少等于 size + 新增元素个数,避免了因扩容不足而导致的多次复制,极大提升了效率。
ArrayList 的成长哲学通过这次源码级的剖析,我们可以总结出 ArrayList 的核心工作原理:
阶段 | 关键动作 | 容量变化 |
|---|---|---|
创建 | 初始化一个长度为 0 的 elementData 数组 | 0 |
首次添加 | 创建一个长度为 10 的新数组 | 10 |
常规扩容 | 容量不足时,创建 1.5 倍长度的新数组并复制 | 10 → 15 → 22 → ... |
批量扩容 | 1.5 倍不够时,以 size + 新增元素数 为准创建新数组 | 10 (满) + 100 → 110 |
ArrayList 的“动态”并非魔法,而是通过延迟初始化和智能扩容策略实现的。它用 size 精确管理元素数量和插入位置,用 1.5 倍的优雅增长平衡内存与性能,并在面对批量操作时展现出“以需为本”的智慧。
理解了这些底层机制,你就能更自信地使用 ArrayList,并预判其在不同场景下的行为。