ArrayList 的扩容机制是怎样的?

2025年 阅读约 7 分钟 面试指南 · Java面试

深入解析ArrayList的扩容机制,从初始容量到1.5倍扩容,分初级、中级、高级三个层次全面讲解。

一句话总结

ArrayList 默认初始容量 10(JDK 7+ 延迟初始化),扩容为 1.5 倍(oldCapacity + oldCapacity >> 1),通过 Arrays.copyOf() 复制到新数组。1.5 倍是空间与时间的折中(比 HashMap 的 2 倍更保守)。预知容量时用 new ArrayList<>(n) 或 ensureCapacity() 避免多次扩容。

初级理解

ArrayList 底层是数组,数组长度固定。当元素数量超过当前数组容量时,需要扩容——创建一个更大的新数组,把旧数组的元素复制过去。

扩容规则:

1. 默认初始容量:10(JDK 7+ 是延迟初始化,第一次 add 时才创建数组)

2. 扩容倍数:新容量 = 旧容量 + 旧容量 / 2,即 1.5 倍

3. 扩容方式:通过 Arrays.copyOf() 创建新数组并复制元素

// ArrayList 扩容核心代码(简化) private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 1.5 倍不够用,直接用所需容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 超大容量处理 elementData = Arrays.copyOf(elementData, newCapacity); }

中级深入

为什么是 1.5 倍?这是空间和时间的折中。倍数太小 → 频繁扩容,性能差;倍数太大 → 浪费内存。1.5 倍是经验值,比 HashMap 的 2 倍更保守(因为数组是连续内存,太大可能分配失败)。

JDK 6 vs JDK 7+ 的区别:

JDK 6:构造时直接创建长度为 10 的数组

JDK 7+:构造时赋值为空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA),第一次 add 时才创建长度为 10 的数组(延迟初始化,节省内存)

ensureCapacity 方法:如果预知要添加大量元素,可以提前调用 ensureCapacity 一次性扩容到位,避免多次扩容。

ArrayList<String> list = new ArrayList<>(); list.ensureCapacity(1000); // 提前扩容,避免多次复制

高级拓展

扩容的性能影响:每次扩容都需要 Arrays.copyOf(),本质是创建新数组 + System.arraycopy()(native 方法,内存复制)。频繁扩容会导致频繁 GC(旧数组被回收)。

最佳实践:

1. 能预估元素数量时,使用 new ArrayList<>(n) 指定初始容量

2. 大数据量时先调用 ensureCapacity

3. 扩容后旧数组不再使用,可以被 GC 回收

ArrayList 的最大容量:理论上最大为 Integer.MAX_VALUE - 8(约 21 亿),但实际受限于 JVM 可用内存。超过后会尝试扩容到 Integer.MAX_VALUE,如果还不够则抛 OutOfMemoryError。

// 扩容次数估算:从默认 10 扩容到 10000 // 10 → 15 → 22 → 33 → 49 → 73 → 109 → 163 → 244 → 366 // → 549 → 823 → 1234 → 1851 → 2776 → 4164 → 6246 → 9369 → 14053 // 共约 18 次扩容,每次都要复制所有元素
面试加分项:能说出 JDK 7 的延迟初始化优化和 ensureCapacity 的使用场景。

实战场景

场景:批量插入优化

// 反例:默认容量,频繁扩容 List<String> list = new ArrayList<>(); for (int i = 0; i < 100000; i++) { list.add("item" + i); // 约 18 次扩容,每次复制所有元素 } // 正例:预估容量,一次到位 List<String> list = new ArrayList<>(100000); for (int i = 0; i < 100000; i++) { list.add("item" + i); // 0 次扩容! } // 正例2:addAll 前 ensureCapacity List<String> list = new ArrayList<>(); list.ensureCapacity(list.size() + otherList.size()); list.addAll(otherList);

面试模拟

Q:ArrayList 扩容为什么是 1.5 倍而不是 2 倍?

A:1.5 倍是空间和时间的折中。倍数太小→频繁扩容性能差;倍数太大→浪费内存。数组需要连续内存,2 倍扩容可能导致大数组分配失败。HashMap 用 2 倍是因为它的数组存的是引用(4/8 字节),内存压力小。

Q:JDK 7 的延迟初始化有什么好处?

A:很多 ArrayList 创建后可能从未添加元素(如作为方法返回值、空集合占位)。JDK 6 构造时直接分配 10 长度的数组,浪费内存。JDK 7+ 构造时赋空数组,第一次 add 才分配,节省了大量内存。