Java 集合框架- List:有序可重复的动态数据容器

在 Java 集合框架中,List 是最常使用的接口之一。它以**“有序、可重复”**为核心特性,为开发者提供了灵活的动态数据存储方案。无论是存储用户列表、操作日志还是实现复杂的业务队列,List 都扮演着不可或缺的角色。本文将从核心特性、典型实现到实战技巧,全面解析 List 接口及其实现类。

一、List 接口的核心特性

List 继承自 Collection 接口,是 Java 中**“有序集合”**的代表。其核心特性可概括为两点:

1. 有序性(Order)

  • 插入顺序与存储顺序一致:元素在 List 中的位置由插入顺序决定,后续插入的元素会排在前面元素之后。
  • 支持索引访问:每个元素都有唯一的索引(从 0 开始),可通过索引直接定位元素(类似数组的 [index] 操作)。

例如:

List<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
System.out.println(list.get(0)); // 输出 "A"(索引 0 对应第一个插入的元素)

2. 可重复性(Duplication)

List 允许存储多个“相等”的元素(通过 equals() 方法判断)。例如:

List<Integer> list = new ArrayList<>();
list.add(10);
list.add(10); // 允许重复添加
System.out.println(list.size()); // 输出 2(包含两个 10)

这与 Set 接口的“不可重复”特性形成鲜明对比,也让 List 更适合存储需要保留重复数据的场景(如用户操作日志中的重复行为)。

二、List 接口的核心方法

ListCollection 接口基础上,新增了一系列针对“索引操作”的方法,核心包括:

方法签名功能描述
void add(int index, E element)在指定索引位置插入元素(后续元素自动后移)
E get(int index)获取指定索引的元素
E set(int index, E element)替换指定索引的元素,返回被替换的旧元素
E remove(int index)删除指定索引的元素,返回被删除的元素
int indexOf(Object o)返回元素 o 第一次出现的索引(无则返回 -1)
int lastIndexOf(Object o)返回元素 o 最后一次出现的索引(无则返回 -1)
List<E> subList(int fromIndex, int toIndex)截取子列表(从 fromIndextoIndex,左闭右开)

这些方法体现了 List 对“有序性”的支持,尤其是 get(int index) 提供了高效的随机访问能力,这是 List 与其他集合(如 SetQueue)的重要区别。

三、List 的典型实现类

List 接口有多个实现类,其中最常用的是 ArrayListLinkedListVector。它们基于不同的数据结构实现,性能特性和适用场景各有侧重。

1. ArrayList:基于动态数组的高效随机访问

ArrayListList 最常用的实现类,底层基于动态数组(可自动扩容的数组)存储元素。
在这里插入图片描述

核心特性:
  • ArrayList 是集合的一个实现类:在 Java 集合框架中,ArrayList是List接口的实现类之一,属于集合体系的一部分,用于存储一组对象。
  • 内部结构与长度特性:ArrayList内部封装了一个Object类型的数组(本质上是基于数组实现的),初始长度为 10。当元素数量超过当前容量时,它会自动扩容(通常是原容量的 1.5 倍),所以长度是可变的,不需要手动管理容量。
  • 性能特点
    • 查询快:因为底层是数组,通过索引可以直接访问元素,所以随机查询(如get(int index))的效率很高。
    • 增删慢:如果在集合中间位置进行添加或删除操作,需要移动大量元素(比如在索引i处添加元素,i之后的元素都要后移一位),所以增删操作效率较低;但如果是在集合尾部进行增删操作,效率还是比较高的。
  • 线程安全性:ArrayList是线程不安全的,在多线程环境下如果没有额外的同步措施,可能会出现数据不一致的问题。如果需要线程安全的列表,可以考虑使用Vector或者Collections.synchronizedList(List)来包装ArrayList。
适用场景:
  • 频繁读取元素(如查询操作多)。
  • 元素数量可预估(初始化时指定容量可减少扩容次数)。
  • 增删操作主要在尾部(无需移动元素)。
示例代码:
// 初始化时指定容量(推荐,减少扩容)
List<String> arrayList = new ArrayList<>(100);
arrayList.add("Java");
arrayList.add("Python");
arrayList.add("C++");

// 随机访问(高效)
String first = arrayList.get(0); // "Java"

// 尾部添加(高效)
arrayList.add("Go");

// 中间插入(低效,需移动元素)
arrayList.add(1, "JavaScript");

2. LinkedList:基于双向链表的高效增删

LinkedList 底层基于双向链表实现,每个元素通过“前驱”和“后继”指针关联。
在这里插入图片描述

核心特性:
  • ArrayList 是集合的一个实现类:在 Java 集合框架中,LinkedList是List接口的实现类之一,属于集合体系的一部分,用于存储一组对象。
  • 内部结构:LinkedList内部封装了一个双向链表(每个节点包含前驱节点引用、元素值、后继节点引用),通过节点之间的引用关系来维护元素的顺序。
  • 性能特点
    • 查询快:在链表的任意位置(头部、中间、尾部)进行添加或删除元素时,只需修改节点的引用关系即可,不需要像数组那样移动大量元素,所以增删操作效率很高。
    • 增删慢:如果要查询指定索引的元素,需要从链表的头节点(或尾节点)开始逐个遍历节点,直到找到目标索引,所以随机查询(如get(int index))的效率较低。
  • 线程安全性:LinkedList是线程不安全的,在多线程环境下如果没有额外的同步措施,可能会出现数据不一致的问题。
适用场景:
  • 频繁在首尾增删元素(如实现队列、栈)。
  • 元素数量不确定,且中间元素操作较少。
示例代码:
List<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");

// 头部添加(高效)
linkedList.add(0, "Head");

// 尾部删除(高效)
linkedList.remove(linkedList.size() - 1); // 删除 "B"

// 作为队列使用(FIFO)
Queue<String> queue = new LinkedList<>();
queue.offer("Task1"); // 入队
queue.poll(); // 出队(返回 "Task1")

3. Vector:线程安全的“老派”实现

Vector 是 Java 早期的 List 实现类,底层同样基于动态数组,但所有方法都被 synchronized 修饰,保证线程安全。
在这里插入图片描述

核心特性:
  • 底层数据结构是:数组:Vector 底层基于数组实现,这一点和 ArrayList 类似。
  • 有索引、查询快、增删慢
    • 因为基于数组,通过索引可直接访问元素,所以查询速度快;
    • 但在增删元素时,需要移动大量数组元素(尤其是中间位置的增删),所以增删操作效率较低。
  • 单线程,线程安全:Vector 是线程安全的集合,其方法大多被 synchronized 修饰,在单线程环境下使用时,由于线程安全的开销,性能会比 ArrayList 稍差;不过在多线程场景中,它能保证数据的一致性,而 ArrayList 是线程不安全的。
适用场景:
  • 仅在低并发、需要线程安全的场景下使用(现在更推荐 Collections.synchronizedList(new ArrayList<>())CopyOnWriteArrayList)。
示例代码:
List<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
int value = vector.get(0); // 线程安全的访问

四、ArrayList 与 LinkedList 的核心区别

特性ArrayListLinkedList
底层结构动态数组双向链表
随机访问(get)O(1)(高效)O(n)(低效)
中间插入/删除O(n)(需移动元素)O(n)(需遍历定位,但移动成本低)
首尾插入/删除O(1)(尾部高效,头部低效)O(1)(高效)
内存占用连续空间,可能有冗余容量非连续空间,额外存储指针
线程安全性非线程安全非线程安全

选择建议

  • 优先用 ArrayList:读多写少、需要随机访问。
  • 优先用 LinkedList:写多(尤其是首尾操作)、无需随机访问。

五、List 的实战避坑指南

1. 避免在循环中使用 ArrayListremove(int index)

ArrayListremove(int index) 会导致后续元素前移,若在 for 循环中直接使用,可能跳过元素:

List<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
// 错误示例:删除元素后索引变化,导致 "C" 被跳过
for (int i = 0; i < list.size(); i++) {
    list.remove(i); 
}
// 正确方式:使用迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    iterator.next();
    iterator.remove();
}

2. subList 不是独立的新集合

List.subList(from, to) 返回的子列表是原列表的视图(共享底层数据),修改子列表会影响原列表:

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3, 4));
List<Integer> subList = list.subList(1, 3); // 子列表 [2, 3]
subList.add(5); 
System.out.println(list); // 输出 [1, 2, 3, 5, 4](原列表被修改)

若需独立子列表,需新建 ArrayList

List<Integer> subList = new ArrayList<>(list.subList(1, 3));

3. 初始化 ArrayList 时指定容量

ArrayList 扩容会创建新数组并复制元素,频繁扩容影响性能。已知元素数量时,指定初始容量可优化:

// 已知约有 1000 个元素,直接初始化容量为 1000
List<User> users = new ArrayList<>(1000);

4. 线程安全问题

ArrayListLinkedList 都是非线程安全的,多线程并发读写可能导致数据错乱或 ConcurrentModificationException。解决方式:

  • 低并发:使用 Collections.synchronizedList(list) 包装。
  • 高并发(读多写少):使用 CopyOnWriteArrayList(写时复制,读无锁)。

六、总结

List 接口以“有序、可重复”为核心,通过 ArrayListLinkedList 等实现类,满足了不同场景下的动态数据存储需求。选择 List 实现类时,需权衡随机访问效率和增删性能:

  • ArrayList 适合读多写少、需要随机访问的场景;
  • LinkedList 适合频繁在首尾操作元素的场景。
    在这里插入图片描述
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐