Java ArrayList:动态数组的“七十二变”

16 阅读5分钟

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

特性ArrayListLinkedList
底层结构动态数组双向链表
随机访问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 飞起来

  1. 预分配容量
    // 已知要存1000个元素?直接 new ArrayList<>(1000) 避免多次扩容。
    
  2. 批量操作优化
    // 用 addAll() 替代循环 add(),减少扩容次数。
    
  3. 慎用 subList
    // subList 是原列表的“视图”,修改会影响原列表(像极了量子纠缠)。
    
  4. 及时 trimToSize()
    // 列表不再修改时调用,释放多余容量(像减肥成功扔掉旧衣服)。
    

六、面试考点:ArrayList 的灵魂拷问

高频问题与解析

  1. 默认初始容量是多少?为什么是10?

    • 答:默认10,历史原因(Java设计者觉得这个数够用又不太浪费)。
  2. 扩容为什么是1.5倍?

    • 答:平衡空间浪费与扩容频率(2倍太浪费,1.2倍扩容太频繁)。
  3. fail-fast 机制是什么?

    • 答:通过 modCount 检测并发修改,发现不一致立即抛异常(防止数据错乱)。
  4. ArrayList 和 Vector 的区别?

    • 答:Vector 线程安全但性能差,ArrayList 反之(Vector 是过气明星)。

七、总结

ArrayList 是 Java 开发者最亲密的战友之一,但亲密不等于无脑使用:

  • 优点:随机访问快、API 丰富、内存紧凑。
  • 缺点:增删效率低、线程不安全。

终极忠告

就像不要用菜刀削苹果,不要在多线程场景裸用 ArrayList!


彩蛋
如果面试官问你“为什么 ArrayList 的 elementData 用 Object 数组?”,请优雅回答:“因为泛型擦除后大家都是 Object 啊!”(真实答案:历史兼容性与泛型实现机制)。