深入浅出 ArrayList:从基础用法到底层原理的全面解析(上)
在 Java 开发中,集合框架是日常编码的 “基础设施”,而 ArrayList 作为其中最常用的 List 实现类,几乎出现在每一个 Java 项目中。无论是存储数据、遍历集合,还是处理动态数据场景,ArrayList 都以其便捷性和高效性成为开发者的首选。但很多人对 ArrayList 的理解仅停留在 “能用来存数据” 的层面,对其底层结构、扩容机制、线程安全等核心问题一知半解。本文将从基础到深入,带你全面掌握 ArrayList 的核心知识,帮你在实际开发中避坑提效。
一、ArrayList 是什么?—— 从定义到底层结构
1.1 ArrayList 的官方定位
ArrayList 是 Java 集合框架中java.util
包下的一个动态数组实现类,它实现了 List 接口,继承自 AbstractList 抽象类,同时还实现了 Cloneable(支持克隆)、Serializable(支持序列化)、RandomAccess(支持随机访问)三个标记接口。
官方文档对 ArrayList 的描述是:“可动态调整大小的数组实现,允许存储所有类型的元素(包括 null),并提供了基于索引的快速访问能力”。简单来说,ArrayList 就是 “能自动扩容的数组”—— 解决了普通数组 “初始化后容量固定,无法动态添加元素” 的痛点。
1.2 ArrayList 与普通数组的区别
很多人会疑惑:“既然有普通数组,为什么还要用 ArrayList?” 其实两者的核心差异体现在 “灵活性” 和 “功能完整性” 上,具体对比如下:
特性 | 普通数组(Array) | ArrayList |
---|---|---|
容量特性 | 初始化时必须指定容量,且不可变 | 容量动态扩展,无需手动指定初始值 |
元素操作 | 仅支持通过索引访问,无内置方法 | 提供 add/remove/get/contains 等丰富方法 |
存储类型 | 支持基本类型(int [])和引用类型 | 仅支持引用类型(存储基本类型需用包装类,如 Integer) |
长度获取 | 通过 “数组名.length”(属性) | 通过 “list.size ()”(方法) |
空元素支持 | 引用类型数组可存 null,基本类型不行 | 支持存储任意数量的 null 元素 |
举个简单例子:如果需要存储一个 “不确定长度的用户列表”,用普通数组会面临 “容量不够时需要手动创建新数组、复制元素” 的麻烦,而 ArrayList 会自动处理扩容,开发者只需专注于 “存数据” 即可。
1.3 ArrayList 的底层结构
ArrayList 的底层是通过一个Object 类型的数组(elementData) 来存储元素的,核心源码如下(基于 JDK 1.8):
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {// 序列化版本号private static final long serialVersionUID = 8683452581122892189L;// 默认初始容量(JDK 1.8中,无参构造初始化时不立即分配,第一次add时才扩容到10)private static final int DEFAULT_CAPACITY = 10;// 空数组(无参构造初始化时使用)private static final Object[] EMPTY_ELEMENTDATA = {};// 默认空数组(区别于EMPTY_ELEMENTDATA,用于无参构造,标记“未初始化”状态)private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};// 底层存储元素的数组(transient修饰:序列化时需手动处理,避免序列化空元素)transient Object[] elementData;// 集合中实际存储的元素个数(注意:不是数组容量)private int size;// 省略其他方法...
}
从源码可以看出:
elementData
是 ArrayList 的 “核心容器”,所有元素都存在这个数组里;size
记录的是 “当前集合中元素的实际数量”,而elementData.length
才是 “数组的容量”(即能容纳的最大元素数);transient
修饰elementData
:因为 ArrayList 的容量可能大于实际元素个数,序列化时只需要保存有值的元素,避免浪费空间,所以 ArrayList 重写了writeObject
和readObject
方法手动处理序列化。
二、ArrayList 核心特性 —— 必须掌握的 4 个关键点
在使用 ArrayList 前,必须先明确它的核心特性,这直接决定了它的适用场景和避坑方向。
2.1 有序性:元素存储顺序与插入顺序一致
ArrayList 是有序集合,这里的 “有序” 指的是 “元素的存储顺序与插入顺序完全一致”,并且支持通过索引(0-based)精确访问元素。
举个例子:
public class ArrayListOrderDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("苹果");list.add("香蕉");list.add("橙子");// 遍历集合:输出顺序与插入顺序一致for (int i = 0; i < list.size(); i++) {System.out.println(i + ":" + list.get(i));}}
}
输出结果:
0:苹果
1:香蕉
2:橙子
这一点与 HashSet(无序)、HashMap(key 无序,JDK 1.8 后 LinkedHashMap 有序)形成鲜明对比,适合需要 “按插入顺序存储和访问” 的场景(如用户操作日志、订单列表)。
2.2 可重复性:允许存储重复元素
ArrayList 允许存储多个相同的元素(包括 null),这一点与 Set 接口(不允许重复元素)完全不同。
示例代码:
public class ArrayListDuplicateDemo {public static void main(String[] args) {List<Integer> list = new ArrayList<>();list.add(10);list.add(20);list.add(10); // 重复元素list.add(null); // 存储nulllist.add(null); // 重复nullSystem.out.println(list); // 输出:[10, 20, 10, null, null]}
}
需要注意的是:当调用contains(Object o)
判断元素是否存在时,ArrayList 会通过equals()
方法比较元素(null 元素会直接判断是否为 null),因此如果存储自定义对象,需要重写equals()
方法才能正确判断重复。
2.3 随机访问:查询效率极高
ArrayList 实现了RandomAccess
接口(标记接口,无实际方法),表示它支持 “随机访问”—— 即通过索引(get(int index)
)直接定位元素,时间复杂度为O(1)。
这是因为底层是数组,数组的内存空间是 “连续的”,通过 “数组首地址 + 索引 × 元素大小” 的计算方式,能直接找到元素的内存位置,无需像 LinkedList(链表结构)那样从头遍历。
示例:查询 ArrayList 和 LinkedList 的效率对比(以 100 万条数据为例)
public class ArrayListAccessSpeedDemo {public static void main(String[] args) {// 初始化ArrayList和LinkedList,各存100万条数据List<Integer> arrayList = new ArrayList<>();List<Integer> linkedList = new LinkedList<>();for (int i = 0; i < 1000000; i++) {arrayList.add(i);linkedList.add(i);}// 测试ArrayList随机访问(访问第50万条数据)long start1 = System.currentTimeMillis();arrayList.get(500000);long end1 = System.currentTimeMillis();System.out.println("ArrayList随机访问时间:" + (end1 - start1) + "ms");// 测试LinkedList随机访问long start2 = System.currentTimeMillis();linkedList.get(500000);long end2 = System.currentTimeMillis();System.out.println("LinkedList随机访问时间:" + (end2 - start2) + "ms");}
}
输出结果(仅供参考):
ArrayList随机访问时间:0ms
LinkedList随机访问时间:35ms
可以看到,ArrayList 的随机访问效率远超 LinkedList,这也是它成为 “查询密集型场景首选” 的核心原因。
2.4 非线程安全:多线程环境下需谨慎
ArrayList不是线程安全的(线程不安全的集合还有 HashMap、HashSet 等),在多线程同时对 ArrayList 进行 “添加 / 删除” 操作时,可能会出现两种问题:
- ConcurrentModificationException(并发修改异常):迭代器遍历过程中,其他线程修改了集合结构;
- 数据不一致:如元素丢失、数组越界等。
示例:多线程下的 ConcurrentModificationException
public class ArrayListThreadSafeDemo {public static void main(String[] args) {List<String> list = new ArrayList<>();// 线程1:添加元素new Thread(() -> {for (int i = 0; i < 1000; i++) {list.add("元素" + i);}}).start();// 线程2:遍历集合new Thread(() -> {Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}}).start();}
}
运行后大概率会抛出异常:
Exception in thread "Thread-1" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)at java.util.ArrayList$Itr.next(ArrayList.java:859)at com.example.ArrayListThreadSafeDemo.lambda$main$1(ArrayListThreadSafeDemo.java:19)
关于线程安全的解决方案,后文会详细讲解,这里先记住:单线程环境用 ArrayList,多线程环境需额外处理线程安全。
三、ArrayList 构造方法详解 ——3 种初始化方式
ArrayList 提供了 3 个常用的构造方法,不同的初始化方式对应不同的使用场景,理解它们的差异能帮助你更合理地初始化 ArrayList,避免不必要的性能消耗。
3.1 无参构造方法:ArrayList ()
无参构造是日常开发中最常用的方式,源码如下(JDK 1.8):
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
这里有个关键细节:JDK 1.8 中,无参构造并不会立即创建容量为 10 的数组,而是将 elementData 赋值为 “空数组 DEFAULTCAPACITY_EMPTY_ELEMENTDATA”,只有当第一次调用add()
方法时,才会触发扩容,将数组容量初始化为 10。
这种 “延迟初始化” 的设计是为了节省内存 —— 如果创建了 ArrayList 但暂时不存元素,就不会占用 10 个 Object 的内存空间(尤其在创建大量空 ArrayList 时,优化效果明显)。
示例:无参构造的使用
// 无参初始化:此时elementData是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组)
List<String> list = new ArrayList<>();
// 第一次add:触发扩容,elementData容量变为10
list.add("第一次添加");
3.2 指定初始容量构造方法:ArrayList (int initialCapacity)
如果提前知道集合大概会存储多少元素,可以用这个构造方法指定初始容量,避免后续频繁扩容(扩容会消耗性能)。
源码如下:
public ArrayList(int initialCapacity) {if (initialCapacity > 0) {// 初始容量>0:创建指定容量的Object数组this.elementData = new Object[initialCapacity];} else if (initialCapacity == 0) {// 初始容量=0:使用空数组EMPTY_ELEMENTDATAthis.elementData = EMPTY_ELEMENTDATA;} else {// 初始容量<0:抛出非法参数异常throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);}
}
适用场景:已知元素数量的场景(如存储 100 个用户信息),示例:
// 提前知道要存100个用户,指定初始容量100,避免扩容
List<User> userList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {userList.add(new User("用户" + i));
}
注意:如果指定的初始容量小于 0,会抛出IllegalArgumentException
,开发中需避免这种错误。
3.3 传入集合构造方法:ArrayList (Collection<? extends E> c)
如果需要将其他集合(如 HashSet、LinkedList)中的元素 “复制” 到 ArrayList 中,可以用这个构造方法,源码如下:
public ArrayList(Collection<? extends E> c) {// 将传入的集合转为数组,赋值给elementDataelementData = c.toArray();// 如果数组长度>0:if ((size = elementData.length) != 0) {// 判断c.toArray()返回的是否是Object[]类型(避免某些集合的toArray()返回子类数组)if (elementData.getClass() != Object[].class) {// 复制为Object[]数组(确保底层是Object[],避免类型转换问题)elementData = Arrays.copyOf(elementData, size, Object[].class);}} else {// 数组长度=0:使用空数组EMPTY_ELEMENTDATAthis.elementData = EMPTY_ELEMENTDATA;}
}
示例:将 HashSet 的元素复制到 ArrayList 中
// 创建HashSet(无序、无重复)
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");// 将HashSet转为ArrayList(转为有序集合)
List<String> list = new ArrayList<>(set);
System.out.println(list); // 输出可能是[a, b, c]或其他顺序(取决于HashSet的哈希分布)
注意:c.toArray()
可能返回非 Object [] 类型的数组(如某些自定义集合),因此源码中会通过Arrays.copyOf
转为 Object [],确保 ArrayList 的底层数组类型正确。