Java ArrayList:动态数组的“七十二变”
——从增删查改到扩容玄学,一篇让你笑中带泪的深度指南
一、ArrayList 是什么?
一句话定义:ArrayList 是 Java 中基于数组的“动态扩容小能手”,能让你像用魔法一样随意增减元素,告别固定数组的“尺寸焦虑”。
核心特性:
- 动态伸缩:容量自动增长,像极了吃饱就膨胀的胃(但扩容要“消化”旧数据)。
- 随机访问快:凭索引直接定位元素,速度堪比闪电侠(时间复杂度 O(1))。
- 线程不安全:多线程操作时,可能上演“数据消失术”或“数组越界惊魂”。
- 泛型支持:可存储任意对象(但别往里塞基本类型,除非你爱用包装类)。
经典比喻:
如果把数组比作固定大小的收纳盒,ArrayList 就是一个能自动买新盒子的“收纳狂魔”——旧盒子装不下?直接换更大的!
二、用法与案例:ArrayList 的日常秀
1. 基础操作
// 创建(默认容量10,像极了社畜的初始存款)
List<String> list = new ArrayList<>();
// 添加元素(容量不够?自动扩容!)
list.add("程序员の头发");
list.add("咖啡");
list.add(1, "BUG"); // 在索引1处插入元素(后面的元素集体右移)
// 删除元素(后面的元素集体左移,像极了早高峰地铁)
list.remove("BUG"); // 按内容删
list.remove(0); // 按索引删
// 遍历(三种姿势任选)
// 直男式for循环
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
// 优雅增强for
for (String item : list) {
System.out.println(item);
}
// 迭代器模式(适合一边遍历一边删)
Iterator<String> it = list.iterator();
while (it.hasNext()) {
if (it.next().contains("头发")) it.remove(); // 删除含“头发”的元素(程序员泪目)
}
2. 实战案例
- 动态排行榜:
List<Player> rankList = new ArrayList<>(); // 实时添加新玩家得分 rankList.add(new Player("码农小张", 999)); rankList.add(new Player("架构老李", 1500)); // 按分数排序(别问,问就是Comparator) rankList.sort((p1, p2) -> p2.getScore() - p1.getScore());
- 批量数据处理:
// 从数据库读取10000条数据(先预估容量防扩容) List<User> users = new ArrayList<>(10000); ResultSet rs = stmt.executeQuery("SELECT * FROM users"); while (rs.next()) { users.add(new User(rs.getInt("id"), rs.getString("name"))); }
三、原理揭秘:ArrayList 的“黑科技”
1. 底层结构
- 核心数组:
Object[] elementData
(存储元素的“秘密基地”)。 - 容量 vs 大小:
- 容量(capacity):数组的实际长度(杯子的容积)。
- 大小(size):已存储元素的数量(杯中的水量)。
默认初始容量是10,但第一次添加元素时才真正创建数组(JDK8优化,避免内存浪费)。
2. 动态扩容
- 触发条件:
size + 1 > elementData.length
(新元素无处安放时)。 - 扩容公式:
新容量 = 旧容量 + 旧容量 / 2
(即1.5倍,数学老师直呼内行)。 - 扩容代价:需将旧数组拷贝到新数组(
System.arraycopy()
默默流泪)。
扩容段子:
程序员问ArrayList:“你为什么每次扩容1.5倍?”
ArrayList答:“因为爱情(性能与空间的平衡)。”
3. 序列化玄学
transient
的妙用:elementData
被标记为transient
,避免序列化空位(节省空间)。- 自定义序列化:通过
writeObject()
和readObject()
仅序列化有效元素(像极了断舍离后的行李箱)。
四、对比与避坑:ArrayList 的“爱恨情仇”
1. ArrayList vs LinkedList
特性 | ArrayList | LinkedList |
---|---|---|
底层结构 | 动态数组 | 双向链表 |
随机访问 | O(1)(闪电侠) | O(n)(乌龟爬) |
插入删除 | O(n)(搬砖工人) | O(1)(外科医生) |
内存占用 | 更紧凑(数组优势) | 更松散(节点开销) |
总结:查询多用 ArrayList,增删多用 LinkedList,纠结就选 ArrayList(毕竟查询更常用)。 |
2. 避坑指南
- 线程安全陷阱:多线程操作需用
Collections.synchronizedList()
或CopyOnWriteArrayList
(否则数据可能“神秘消失”)。 - 迭代器修改异常:遍历时直接调用
add()
/remove()
会触发ConcurrentModificationException
(改用迭代器的remove()
)。 - 无效初始化:
// 错误示范:new ArrayList(100) 初始容量是100,但 size() 仍是 0! List<Integer> list = new ArrayList<>(100); list.add(1); // size 变 1,但 elementData.length 是 100
五、最佳实践:让 ArrayList 飞起来
- 预分配容量:
// 已知要存1000个元素?直接 new ArrayList<>(1000) 避免多次扩容。
- 批量操作优化:
// 用 addAll() 替代循环 add(),减少扩容次数。
- 慎用 subList:
// subList 是原列表的“视图”,修改会影响原列表(像极了量子纠缠)。
- 及时 trimToSize():
// 列表不再修改时调用,释放多余容量(像减肥成功扔掉旧衣服)。
六、面试考点:ArrayList 的灵魂拷问
高频问题与解析
-
默认初始容量是多少?为什么是10?
- 答:默认10,历史原因(Java设计者觉得这个数够用又不太浪费)。
-
扩容为什么是1.5倍?
- 答:平衡空间浪费与扩容频率(2倍太浪费,1.2倍扩容太频繁)。
-
fail-fast 机制是什么?
- 答:通过
modCount
检测并发修改,发现不一致立即抛异常(防止数据错乱)。
- 答:通过
-
ArrayList 和 Vector 的区别?
- 答:Vector 线程安全但性能差,ArrayList 反之(Vector 是过气明星)。
七、总结
ArrayList 是 Java 开发者最亲密的战友之一,但亲密不等于无脑使用:
- 优点:随机访问快、API 丰富、内存紧凑。
- 缺点:增删效率低、线程不安全。
终极忠告:
就像不要用菜刀削苹果,不要在多线程场景裸用 ArrayList!
彩蛋:
如果面试官问你“为什么 ArrayList 的 elementData 用 Object 数组?”,请优雅回答:“因为泛型擦除后大家都是 Object 啊!”(真实答案:历史兼容性与泛型实现机制)。