ElIauk

Java 集合入门:ArrayList 与 LinkedList

List 是什么 List 是 Collection 家族里最常用的一种:有序、可重复、能通过下标访问。初学者最先接触的通常是 ArrayList,面试里又总被问 ArrayList 和 LinkedList 的区别。与其背结论,不如先弄清楚它们各自把数据存在哪里。 实际项目里 90% 的场景用 A

评论
Java 集合入门:ArrayList 与 LinkedList

List 是什么

List 是 Collection 家族里最常用的一种:有序、可重复、能通过下标访问。初学者最先接触的通常是 ArrayList,面试里又总被问 ArrayList 和 LinkedList 的区别。与其背结论,不如先弄清楚它们各自把数据存在哪里。

实际项目里 90% 的场景用 ArrayList 就够了,LinkedList 出现的频率比想象中低。真正需要队列或栈时,ArrayDeque 往往比 LinkedList 更合适。

ArrayList:数组的包装

ArrayList 内部就是一个 Object[]。第一次 add 时才会分配默认长度 10 的数组,装不下时按 1.5 倍扩容,把老数组复制到新数组。所以:

  • 按下标 get / set 是 O(1),非常快;
  • 中间插入、删除要移动后面所有元素,是 O(n);
  • 尾部 add 均摊 O(1),但扩容那一次会明显变慢。
import java.util.ArrayList;
import java.util.List;

public class ArrayListDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(16); // 预估容量,少扩容
        names.add("Amy");
        names.add("Bob");
        names.add(0, "Cindy");            // 头部插入,后面元素整体后移
        System.out.println(names);        // [Cindy, Amy, Bob]
        System.out.println(names.get(1)); // Amy
        names.remove("Bob");
        System.out.println(names.size()); // 2
    }
}

remove 有两个重载:remove(int index) 按下标删,remove(Object o) 按内容删。List<Integer> 里写 list.remove(1) 删的是下标 1,不是值为 1 的元素,这个坑非常经典。

扩容时要申请新数组并复制全部元素,这也是为什么能预估大小时一定要传初始容量。ArrayList 不是线程安全的,多线程同时 add 可能丢元素,甚至抛数组越界异常;并发读多的场景用 CopyOnWriteArrayList,写多的场景在外层加锁或者换用队列。

LinkedList:双向链表

LinkedList 的每个节点都保存前驱和后继指针。头尾增删是 O(1),按下标访问要顺着链表走,是 O(n)。它比 ArrayList 多存两个引用,内存占用更大,缓存局部性也差,所以“ LinkedList 插入删除更快”这句话只在“已经定位到节点”的前提下成立。

真要用链表结构,先想清楚是不是队列或栈:是的话用 ArrayDeque,它基于循环数组,性能通常更好,还禁止存 null。

LinkedList 每个节点额外存两个指针,内存开销大约是 ArrayList 的两三倍,遍历时缓存命中率也低。除非确实要在头部频繁插入,否则没必要为了“链表插入快”这个直觉选它;真要在中间频繁插入,先确认自己是否真能拿到那个位置的节点。另外,Vector 和 Stack 属于老 API,方法上加了 synchronized,性能一般,新代码不要再用。

怎么选

场景 推荐 原因
按位置读取、遍历为主 ArrayList 数组随机访问快,内存连续
只在尾部追加 ArrayList 均摊 O(1)
频繁在头部插入 LinkedList 头插 O(1)
当队列/栈用 ArrayDeque 比 LinkedList 更轻量
读多写少的并发场景 CopyOnWriteArrayList 读不加锁,写时复制

常见坑

1. Arrays.asList 返回的是定长列表

List<String> list = Arrays.asList("a", "b");
list.add("c"); // UnsupportedOperationException

它返回的是 Arrays 内部的私有实现,底层还是原数组,只能改不能增删。需要可变列表就再包一层:new ArrayList<>(Arrays.asList("a", "b"))。

2. subList 是视图不是副本

list.subList(1, 3) 拿到的是原列表的一段“窗口”。之后只要原列表被结构性修改(增删元素),再访问这个窗口就会抛 ConcurrentModificationException。想长期保存,复制出来:new ArrayList<>(list.subList(1, 3))。

3. 遍历时删除元素

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if ("b".equals(it.next())) {
        it.remove();
    }
}

增强 for 里直接 list.remove() 会抛 ConcurrentModificationException。更简洁的写法是 list.removeIf("b"::equals),JDK 8 之后都支持。

4. 忘记指定初始容量

默认容量是 10,存 1000 条数据要扩容七八次,每次都要复制数组。能预估大小时写成 new ArrayList<>(expectedSize),性能会稳定很多。

5. 用 contains 做去重

List.contains 是 O(n),外层再套一层循环就是 O(n²)。数据量上千就该换 HashSet,需要保留顺序用 LinkedHashSet。

排查步骤

  1. 看到 UnsupportedOperationException,先确认 List 是不是 Arrays.asList、List.of 或 Collections.unmodifiableList 产生的;
  2. 看到 ConcurrentModificationException,检查遍历过程中有没有增删元素;
  3. 接口返回 List 时不要返回 null,没有数据就返回空列表,调用方可以少一次判空;
  4. 性能问题先看有没有在循环里调用 contains、indexOf;
  5. 数据量超过一万时,优先考虑换数据结构,而不是继续调 ArrayList 的参数。

参考

评论

推荐阅读

订阅邮件

订阅更新,及时收到最新文章。

作者