孤舟笔记 Java 集合篇一 ArrayList是如何实现自动扩容的?这个机制藏着大智慧
文章目录一、先说结论ArrayList 扩容核心事实二、初始容量不是你以为的 10三、扩容核心1.5 倍是怎么算的四、扩容的性能代价五、和 Vector 扩容的区别ArrayList 自动扩容 全景回答技巧与点评标准回答加分回答面试官点评个人网站你天天用 ArrayList但面试官一问ArrayList 怎么扩容的你就只能说每次扩大 1.5 倍——再追问1.5 倍怎么算的、“扩容时数组怎么搬”、“初始容量是多少”就答不上来了。今天咱们把 ArrayList 的扩容机制从底层源码讲透。一、先说结论ArrayList 扩容核心事实维度说明底层结构Object[] 数组默认初始容量0首次 add 扩为 10扩容触发元素个数 数组长度时扩容比例新容量 旧容量 × 1.5右移 1 位扩容操作Arrays.copyOf() 复制到新数组时间复杂度均摊 O(1)单次扩容 O(n)一句话记住ArrayList 扩容像搬家——房子住不下了找个 1.5 倍大的新房子把东西搬过去。二、初始容量不是你以为的 10// JDK 8 源码privatestaticfinalintDEFAULT_CAPACITY10;privatestaticfinalObject[]DEFAULTCAPACITY_EMPTY_ELEMENTDATA{};publicArrayList(){this.elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA;// 空数组不是 10}无参构造时内部数组是空的容量是 0。那什么时候变成 10首次 add 时publicbooleanadd(Ee){ensureCapacityInternal(size1);// 检查是否需要扩容elementData[size]e;returntrue;}privatevoidensureCapacityInternal(intminCapacity){if(elementDataDEFAULTCAPACITY_EMPTY_ELEMENTDATA){minCapacityMath.max(DEFAULT_CAPACITY,minCapacity);// 首次 add至少扩到 10}ensureExplicitCapacity(minCapacity);}生活类比ArrayList 像懒人租房——先不租等要住的时候再租一个 10 间的。构造方式初始容量new ArrayList()0首次 add → 10new ArrayList(20)20new ArrayList(otherList)otherList.size()三、扩容核心1.5 倍是怎么算的当元素个数超过数组长度触发扩容privatevoidgrow(intminCapacity){intoldCapacityelementData.length;intnewCapacityoldCapacity(oldCapacity1);// 右移 1 位 除以 2 → 1.5 倍if(newCapacity-minCapacity0)newCapacityminCapacity;// 1.5 倍还不够直接用需要的容量if(newCapacity-MAX_ARRAY_SIZE0)newCapacityhugeCapacity(minCapacity);// 超大容量处理elementDataArrays.copyOf(elementData,newCapacity);// 搬家}1.5 倍的数学oldCapacity (oldCapacity 1)old old/21.5 × old为什么用位运算右移 1 位比除以 2 快这是底层代码的性能优化。扩容流程1. 计算新容量 旧容量 × 1.5 2. 如果 1.5 倍还不够如 addAll 大量元素直接用所需容量 3. 创建新数组 4. Arrays.copyOf 复制旧数据到新数组 这步 O(n) 5. elementData 指向新数组生活类比搬家流程——算新房子要多大1.5 倍租新房子把旧家具搬过去旧房子退租。四、扩容的性能代价单次扩容 O(n)需要复制整个数组。// 模拟连续 add 100 万个元素ArrayListIntegerlistnewArrayList();// 初始容量 0// 扩容次数10 → 15 → 22 → 33 → ... → 大约 24 次// 每次扩容都要复制整个数组但均摊 O(1)把扩容代价分摊到每次 add平均每次 add 只需 O(1)。假设当前容量 Cadd 一个元素触发扩容 - 复制 C 个元素O(C) - 接下来可以 add C/2 个元素不需要扩容 - 均摊O(C) / (C/2) O(1)优化建议如果预知元素数量指定初始容量避免多次扩容// ✅ 已知要放 10000 个元素ArrayListIntegerlistnewArrayList(10000);// 一次到位// ❌ 默认初始容量多次扩容ArrayListIntegerlistnewArrayList();// 扩容约 17 次五、和 Vector 扩容的区别维度ArrayListVector扩容比例1.5 倍2 倍线程安全❌✅synchronized扩容增量0不可自定义可通过 capacityIncrement 自定义// Vector 的 growintnewCapacityoldCapacity((capacityIncrement0)?capacityIncrement:oldCapacity);// 默认 2 倍Vector 扩 2 倍更浪费空间ArrayList 扩 1.5 倍更节省。这也是 ArrayList 取代 Vector 的原因之一。ArrayList 自动扩容 全景ArrayList 自动扩容 全景 初始容量 ├── 无参构造 → 0首次 add → 10 ├── 指定容量 → 指定值 └── 传入集合 → 集合 size 扩容机制 ├── 触发条件 ── size 1 elementData.length ├── 扩容比例 ── 1.5 倍oldCapacity 1 ├── 兜底处理 ── 1.5 倍不够则直接用 minCapacity ├── 搬家操作 ── Arrays.copyOf() └── 均摊复杂度 ── O(1) 与 Vector 对比 ├── 扩容比例 ── 1.5 倍 vs 2 倍 ├── 线程安全 ── ❌ vs ✅ └── 空间浪费 ── 更少 vs 更多 口诀首次 add 容量十一点五倍来扩容 右移一位除以二Arrays.copyOf 把家搬 预知大小先指定省得扩容费功夫。回答技巧与点评标准回答ArrayList 底层基于 Object[] 数组实现默认初始容量为 0首次 add 时扩容为 10。当元素个数超过数组长度时触发扩容新容量为旧容量的 1.5 倍通过 oldCapacity (oldCapacity 1) 计算。如果 1.5 倍仍不够如 addAll 大量元素则直接使用所需容量。扩容通过 Arrays.copyOf() 将旧数组复制到新数组单次扩容 O(n)但均摊到每次 add 为 O(1)。建议预知元素数量时指定初始容量避免多次扩容。加分回答1.5 倍 vs 2 倍的权衡ArrayList 选择 1.5 倍而不是 2 倍是空间和时间的权衡。2 倍扩容能保证均摊 O(1)但浪费更多空间1.5 倍虽然也能均摊 O(1)但空间利用率更高。实际上 1.5 倍还有一个好处通过 GC 回收的旧数组内存可以被复用——因为新数组不会永远比旧数组大太多懒初始化的设计JDK 8 把无参构造的初始容量从 10 改为 0懒初始化是为了节省空 ArrayList 的内存。很多 ArrayList 创建后从未使用空数组比 10 长度数组省内存。这是一个典型的延迟初始化优化SubList 的坑ArrayList.subList() 返回的是原列表的视图不是新列表。对 SubList 的修改会直接影响原列表扩容时也会互相影响。如果原列表结构被修改如 addSubList 会抛 ConcurrentModificationException面试官点评这道题考的是你对集合底层实现的了解。能说出1.5 倍扩容、Arrays.copyOf是基本要求能讲清楚初始容量为 0 的懒初始化、1.5 倍的位运算实现、均摊 O(1) 的分析才算及格。如果你能从设计角度分析 1.5 倍 vs 2 倍的权衡、懒初始化的优化动机面试官会认为你不只是背了源码还理解了设计背后的思考。原文阅读内容有帮助点赞、收藏、关注三连评论区等你