
上一篇讲了数组和链表的基础概念,这篇直接进入正题——ArrayList。
面试里关于 ArrayList 的问题,问法五花八门,但基本都绕不开这几类:
ArrayList 底层是怎么实现的? 它的扩容机制是什么样的? new ArrayList(10)扩容了几次? 数组和 List 怎么互相转换,转换之后互相修改有没有影响?
这些问题的答案全在源码里。JDK 1.8 的 ArrayList 源码不算复杂,跟着走一遍就能搞明白。
ArrayList 的底层数据结构
一句话:ArrayList 底层是用动态数组实现的。
transient Object[] elementData;private int size;
elementData 就是那个存数据的数组,size 是实际装了多少元素。注意 size 和 elementData.length 是两回事——前者是"装了多少",后者是"能装多少"(容量)。
成员变量
先看 ArrayList 里几个关键的成员变量:
// 默认初始容量private static final int DEFAULT_CAPACITY = 10;// 空数组实例,用于指定容量为 0 时private static final Object[] EMPTY_ELEMENTDATA = {};// 空数组实例,用于无参构造,跟 EMPTY_ELEMENTDATA 区分开// 目的是:第一次添加元素时,知道该扩容到 DEFAULT_CAPACITY(10)private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 真正存数据的数组transient Object[] elementData;// 实际元素个数private int size;
DEFAULTCAPACITY_EMPTY_ELEMENTDATA 和 EMPTY_ELEMENTDATA 虽然都是空数组,但用途不同。源码用前者来标记"这个 ArrayList 是通过无参构造创建的",这样第一次 add 时才能把容量初始化为默认的 10。

构造方法
ArrayList 有三个构造方法:
// 无参构造:空集合,第一次 add 时才初始化容量为 10publicArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;}// 指定初始容量publicArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}}// 从其他集合创建publicArrayList(Collection<? extends E> c) {Object[] a = c.toArray();if ((size = a.length) != 0) {if (c.getClass() == ArrayList.class) {elementData = a;} else {elementData = Arrays.copyOf(a, size, Object[].class);}} else {elementData = EMPTY_ELEMENTDATA;}}
无参构造是最常用的,注意它并没有直接创建长度为 10 的数组,而是用了"懒加载"——等你真的往里面放东西时才分配空间。
add 方法:添加元素的全过程
public boolean add(E e) {ensureCapacityInternal(size + 1); // 确保容量够用elementData[size++] = e; // 放入元素,size++return true;}
整个 add 的核心在 ensureCapacityInternal 这一行。它内部调了三个方法,逐层拆开来看:
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));}// 计算容量:如果是默认空数组,至少给 DEFAULT_CAPACITYprivate static int calculateCapacity(Object[] elementData, int minCapacity) {if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}return minCapacity;}// 如果需要的容量大于当前数组长度,扩容private void ensureExplicitCapacity(int minCapacity) {modCount++; // 结构化修改计数,用于 fail-fastif (minCapacity - elementData.length > 0)grow(minCapacity);}
源码里那个 modCount++ 不是可有可无的。modCount 记录的是 ArrayList 结构化修改的次数——添加、删除、扩容等改变数组结构的操作。它的核心用途是 fail-fast 机制:
// 用 for-each 或迭代器遍历时,如果另一个线程修改了 ArrayList// 迭代器的 next() 方法会检查 modCount 是否和预期值一致// 不一致就直接抛 ConcurrentModificationException,立刻失败
这样做的好处是"宁可快速报错,也不带着脏数据继续执行"。比如你在遍历一个 List 的时候,另一个线程删除了一个元素,如果没有 fail-fast,你可能读到不一致的状态,排查起来比直接抛异常麻烦得多。
注意,fail-fast 并不能保证一定捕获并发修改——它只是"尽力而为"的检测。所以不要在遍历时直接调用 list.remove(),要用迭代器的 remove() 方法,或者用 CopyOnWriteArrayList 这类并发安全的集合。
把整个添加流程用 mermaid 画出来:

扩容机制:grow 方法
privatevoidgrow(int minCapacity) {int oldCapacity = elementData.length;int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);elementData = Arrays.copyOf(elementData, newCapacity); // 数组拷贝}
核心公式:newCapacity = oldCapacity + (oldCapacity >> 1),也就是 原来的 1.5 倍。
每次扩容都会调用 Arrays.copyOf,这会创建一个新数组,然后把老数组里的元素挨个拷过去。这个过程是 O(n) 的。

扩容过程的三个阶段
用 new ArrayList() 无参构造,然后依次添加元素,容量变化分三个阶段。PPT 里专门用了 3 页来演示这个过程:
阶段一:第 1 次 add

关键点:无参构造的 ArrayList,第一次 add 时因为 elementData 还是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,calculateCapacity 直接返回 DEFAULT_CAPACITY=10,触发扩容到 10。
阶段二:第 2~10 次 add

第 2 到第 10 次 add,每次需要的容量(2, 3, 4, …, 10)都不超过当前数组长度 10,所以一次都不扩容。10 次 add 总共扩容 1 次。
阶段三:第 11 次 add(触发扩容)

size 从 10 变成 11,超过了当前容量 10,触发扩容。10 + (10 >> 1) = 15,新容量变成 15。
把三个阶段串起来:
再往后,第 16 次 add 会再次扩容到 22(15 + 7),以此类推。每次都是容量不够了才扩,扩完是原来的 1.5 倍。
扩容的边界处理:hugeCapacity
grow 方法里还有一个细节容易被忽略:
if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);
MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8。数组在 JVM 里有一些 header 开销,所以实际能分配的最大数组长度不是 Integer.MAX_VALUE,而是减掉 8 字节的 header。
当新容量超过这个阈值时,会调用 hugeCapacity:
privatestaticinthugeCapacity(int minCapacity) {if (minCapacity < 0) // 溢出,说明 minCapacity 已经超过了 Integer.MAX_VALUEthrow new OutOfMemoryError();return (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;}
简单说就是:正常情况下扩容到 MAX_ARRAY_SIZE 就封顶;极端情况下(minCapacity 在 MAX_ARRAY_SIZE 和 Integer.MAX_VALUE 之间)允许扩到 Integer.MAX_VALUE;真的超了 Integer.MAX_VALUE 就抛 OutOfMemoryError。
所以如果预先知道要存多少数据,最好在构造时就指定初始容量,避免反复扩容和数组拷贝。
new ArrayList(10) 扩容了几次?
这是一个经典面试题。
publicArrayList(int initialCapacity) {if (initialCapacity > 0) {this.elementData = new Object[initialCapacity]; // 直接创建容量为 10 的数组} else if (initialCapacity == 0) {this.elementData = EMPTY_ELEMENTDATA;} else {throw new IllegalArgumentException(...);}}
带参数的构造函数直接 new Object[10],创建出来容量就是 10,存了数组就知道自己有多大。add 元素时,只有 size 超过 10 之后才会触发第一次扩容。
所以答案是:0 次。new ArrayList(10) 只是声明和实例化了一个 ArrayList,指定了容量为 10,没有发生扩容。 等你 add 第 11 个元素时才会触发第一次扩容(10 × 1.5 = 15)。
和 new ArrayList() 的区别:

数组与 List 互转
数组转 List:Arrays.asList
String[] strs = {"aaa", "bbb", "ccc"};List<String> list = Arrays.asList(strs);
这个方法返回的不是 java.util.ArrayList,而是 Arrays 内部的一个私有静态类 ArrayList。它底层直接引用原数组,没有拷贝数据。
// 修改数组的内容strs[1] = "ddd";System.out.println(list); // [aaa, ddd, ccc] —— list 也变了!
因为底层用的是同一个数组,所以修改数组,List 会受影响。
注意:Arrays.asList 返回的 List 不支持 add/remove 操作。想得到一个真正独立的 ArrayList,应该这样写:
List<String> list = new ArrayList<>(Arrays.asList(strs));List 转数组:toArray
List<String> list = new ArrayList<>();list.add("aaa");list.add("bbb");list.add("ccc");// 方式一:无参,返回 Object[]Object[] arr1 = list.toArray();// 方式二:指定类型,更常用String[] arr2 = list.toArray(new String[0]);
无参 toArray 返回 Object 数组,需要自己强转。推荐用带参版本,直接拿到正确的类型。
关键点:toArray 底层做了数组拷贝,返回的是一个全新的数组。
String[] array = list.toArray(new String[list.size()]);list.add("ddd");System.out.println(Arrays.toString(array)); // [aaa, bbb, ccc] —— 不受影响
List 修改之后,toArray 生成的数组不受影响,因为底层拷贝了一份。

ArrayList 是线程不安全的
ArrayList 的方法没加 synchronized,多线程并发 add 会出问题。
典型场景:两个线程同时 add,都读到 size = 5,都往 elementData[5] 写数据,结果一个元素被覆盖。或者扩容时,线程 A 正在 Arrays.copyOf,线程 B 读到一半拷贝了一半没拷贝的数组,直接崩。
需要线程安全的 List 有两种方案:
// 方案一:Collections.synchronizedList 包装List<String> syncList = Collections.synchronizedList(new ArrayList<>());// 方案二:直接用 CopyOnWriteArrayListList<String> cowList = new CopyOnWriteArrayList<>();
synchronizedList 是给所有方法加锁,写操作性能一般。CopyOnWriteArrayList 是写时复制,适合读多写少的场景。
面试模板
如果面试官问"说一下 ArrayList 的底层实现原理",可以这样回答:
ArrayList 底层是用动态数组实现的。成员变量 elementData 是 Object 数组,size 记录实际元素个数。
无参构造时,并没有直接创建数组,而是把 elementData 指向一个共享的空数组实例。第一次 add 时,才会把容量初始化为默认的 10。
add 方法先调用 ensureCapacityInternal 检查容量是否够用,不够就调 grow 扩容。扩容大小是原来容量的 1.5 倍,通过 oldCapacity + (oldCapacity >> 1) 计算,底层用 Arrays.copyOf 把老数据拷贝到新数组。
如果构造时指定了初始容量,比如 new ArrayList(10),就直接创建长度为 10 的数组,在存满之前不需要扩容。数组转 List 用 Arrays.asList,它底层直接引用原数组,互相修改会影响。List 转数组用 toArray,底层做了拷贝,所以互不影响。
ArrayList 不是线程安全的,多线程场景可以用 Collections.synchronizedList 包装或者用 CopyOnWriteArrayList。
夜雨聆风