什么是 ArrayList?
ArrayList 是 Java 集合框架中一个非常核心的类,它的中文意思是“动态数组”或“数组列表”。

核心特点:
- 动态大小:与 Java 中固定长度的普通数组(
int[])不同,ArrayList的长度是可变的,你可以在运行时随时添加或删除元素,它的大小会自动调整。 - 基于数组实现:尽管它叫“动态数组”,但其底层实现仍然是数组,当数组空间不足时,它会创建一个更大的新数组,并将旧数组中的元素复制到新数组中。
- 有序:
ArrayList中的元素是按照你添加的顺序(插入顺序)进行存储的。 - 允许重复元素:你可以向
ArrayList中添加多个相同的元素。 - 允许 null 元素:
ArrayList可以存储null值。
你可以把它想象成一个可以自动伸缩的盒子,你可以随意往里面放东西,不用提前考虑盒子的大小。
为什么使用 ArrayList 而不是普通数组?
| 特性 | 普通数组 (int[]) |
ArrayList |
|---|---|---|
| 大小 | 固定,创建时必须指定长度,之后无法改变。 | 动态可变,可以随时添加或删除元素。 |
| 类型 | 可以是基本类型(如 int[]),也可以是对象(如 String[])。 |
只能存储对象,不能直接存储基本类型,但可以使用其包装类(如 Integer)。 |
| 性能 | 访问(get)速度快,因为基于索引,增删慢,因为需要移动元素。 | 访问(get)速度快,因为底层是数组,增删在末尾时快,在中间/开头时慢(需要移动元素)。 |
| 功能 | 功能有限,只有 length 属性和一些基本方法。 |
功能丰富,提供了大量操作列表的方法(如 add(), remove(), size() 等)。 |
| 代码 | String[] arr = new String[10]; |
ArrayList<String> list = new ArrayList<>(); |
当你需要一个可以动态改变大小的集合,并且需要频繁地通过索引访问元素时,ArrayList 是最佳选择。
如何使用 ArrayList (常用方法)
在使用 ArrayList 之前,你需要导入它所在的包:
import java.util.ArrayList;
1 创建和初始化
// 1. 创建一个空的 ArrayList,默认初始容量是 10 ArrayList<String> names = new ArrayList<>(); // 2. 创建一个指定初始容量的 ArrayList (推荐,如果预知大小,可以减少扩容带来的性能损耗) ArrayList<Integer> numbers = new ArrayList<>(20); // 3. 使用 Arrays.asList 初始化 (注意:返回的是固定大小的列表,不能增删) ArrayList<Double> prices = new ArrayList<>(Arrays.asList(10.5, 20.0, 15.8));
2 常用操作方法
假设我们有以下 ArrayList:
ArrayList<String> fruits = new ArrayList<>();
| 方法 | 描述 | 示例 |
|---|---|---|
add(E element) |
向列表末尾添加一个元素。 | fruits.add("Apple"); |
add(int index, E element) |
在指定位置插入一个元素。 | fruits.add(0, "Banana"); // 在开头插入 |
get(int index) |
获取指定索引位置的元素。 | String firstFruit = fruits.get(0); // "Banana" |
set(int index, E element) |
修改指定索引位置的元素。 | fruits.set(1, "Orange"); // 将第二个元素改为 "Orange" |
remove(int index) |
移除指定索引位置的元素,并返回该元素。 | fruits.remove(0); // 移除 "Banana" |
remove(Object obj) |
移除列表中第一个出现的指定元素。 | fruits.remove("Orange"); |
size() |
返回列表中的元素个数(类似数组的 length)。 |
int count = fruits.size(); |
clear() |
清空列表中的所有元素。 | fruits.clear(); |
isEmpty() |
判断列表是否为空,如果为空返回 true。 |
boolean empty = fruits.isEmpty(); |
contains(Object obj) |
判断列表中是否包含某个元素,包含返回 true。 |
boolean hasApple = fruits.contains("Apple"); |
indexOf(Object obj) |
返回元素第一次出现的索引,如果不存在返回 -1。 | int index = fruits.indexOf("Apple"); |
3 遍历 ArrayList
有三种主要的方式来遍历 ArrayList:
for 循环 (通过索引)
ArrayList<String> fruits = new ArrayList<>(Arrays.asList("Apple", "Banana", "Cherry"));
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
for-each 循环 (推荐)
这是最简洁、最常用的方式,适用于大多数场景。
for (String fruit : fruits) {
System.out.println(fruit);
}
使用迭代器 (Iterator)
迭代器是一种更安全、更通用的方式,特别是在遍历过程中需要删除元素时。
Iterator<String> iterator = fruits.iterator();
while (iterator.hasNext()) {
String fruit = iterator.next();
if ("Banana".equals(fruit)) {
iterator.remove(); // 安全地删除当前元素
}
}
注意:在 for-each 循环中使用
list.remove()是不安全的,可能会导致ConcurrentModificationException,而使用迭代器的remove()方法是安全的。
ArrayList 的底层原理与性能分析
理解 ArrayList 的底层原理有助于更好地使用它。
1 核心属性
ArrayList 内部主要有两个核心属性:
Object[] elementData: 一个对象数组,用于真正存储数据。int size: 记录当前列表中元素的个数。
2 扩容机制
这是 ArrayList 最关键的部分。
- 当你调用
add()方法添加一个元素时,它会检查size是否已经等于elementData.length(即数组是否已满)。 - 如果未满:直接将新元素添加到
elementData[size]的位置,size++。 - 如果已满:触发扩容。
- 计算新的容量:新容量通常是旧容量的 1.5 倍(
newCapacity = oldCapacity + (oldCapacity >> 1))。 - 创建一个这个新容量大小的新数组。
- 将旧数组中的所有元素复制到新数组中。
- 将
elementData引用指向这个新数组。 - 将新元素添加进去,
size++。
- 计算新的容量:新容量通常是旧容量的 1.5 倍(
扩容是一个比较耗时的操作,因为它涉及到数组的创建和复制。
3 性能分析
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
add(E e) |
平均 O(1),最坏 O(n) | 平均情况是 O(1),因为添加到末尾很快,最坏情况发生在扩容时,需要复制所有元素,所以是 O(n)。 |
add(int index, E e) |
O(n) | 因为需要在指定位置插入元素,该位置之后的所有元素都需要向后移动一位。 |
get(int index) |
O(1) | 直接通过索引访问数组,速度极快,这是 ArrayList 最大的优势之一。 |
remove(int index) |
O(n) | 需要将被删除元素之后的所有元素向前移动一位来填补空缺。 |
remove(Object o) |
O(n) | 需要先遍历列表找到该元素的索引,然后再执行删除操作(移动元素)。 |
contains(Object o) |
O(n) | 需要遍历整个列表进行比较。 |
ArrayList 与 LinkedList 的区别
这是面试中非常经典的问题。
| 特性 | ArrayList |
LinkedList |
|---|---|---|
| 数据结构 | 动态数组 | 双向链表 |
| 内存布局 | 连续的内存空间 | 不连续,每个节点(Node)存储数据、前驱指针和后继指针 |
随机访问 (get) |
快 O(1) | 慢 O(n),需要从头或尾开始遍历 |
头部增删 (add(0, e), remove(0)) |
慢 O(n),需要移动所有元素 | 快 O(1),只需修改头节点的指针 |
尾部增删 (add(e), remove(size-1)) |
快 O(1) (不考虑扩容) | 快 O(1) |
| 内存消耗 | 较低,只需要一个数组对象 | 较高,每个元素都是一个对象,额外存储了指针信息 |
| 适用场景 | 频繁查询,较少增删的场景。 | 频繁增删,较少查询的场景。 |
如何选择?
- 如果你的应用场景主要是读取数据,或者你知道大概的数据量,选择
ArrayList。 - 如果你的应用场景主要是在列表中间或头部频繁插入和删除数据,选择
LinkedList。
泛型
你一定注意到了 ArrayList<String> 这种写法,这就是泛型。
作用:
- 类型安全:编译器会检查你放入
ArrayList的元素类型是否正确。ArrayList<String>只能存放String类型的对象,放入Integer会在编译时报错。 - 避免强制类型转换:从
ArrayList中取出元素时,不需要再进行强制类型转换,代码更简洁、更安全。
不使用泛力的旧式写法 (不推荐):
// 可以存放任何 Object 类型的对象
ArrayList list = new ArrayList();
list.add("Hello");
list.add(123); // 编译器不会报错,但逻辑上很混乱
String str = (String) list.get(0); // 取出时必须强制转换
Integer num = (Integer) list.get(1);
ArrayList 是 Java 开发者必须掌握的工具,记住它的核心思想:一个基于数组、大小可变的动态列表。
- 优点:查询快,内存占用相对较低。
- 缺点:在中间或头部增删慢,扩容时有性能开销。
- 核心方法:
add(),get(),remove(),size(),contains()。 - 最佳实践:尽量使用
for-each循环遍历,如果预知大小,在初始化时指定容量,以减少扩容次数。
