Java 中 List 接口详解:知识点与注意事项

发布于:2025-08-13 ⋅ 阅读:(10) ⋅ 点赞:(0)

一、List 接口的定义与特性

List接口位于java.util包下,是Java集合框架中最常用的接口之一,定义为:

public interface List<E> extends Collection<E>

其核心特性如下:

  1. 有序性(Ordered):

    • 元素的存储顺序与插入顺序严格一致
    • 通过从0开始的整数索引(index)可以精确访问元素
    • 提供了基于索引的操作方法,如:
      E get(int index);
      E set(int index, E element);
      void add(int index, E element);
      E remove(int index);
      

    • 示例:添加顺序为A、B、C的元素,遍历时顺序保证为A→B→C
  2. 可重复性(Allow Duplicates):

    • 允许存储多个equals()方法返回true的元素
    • 判断重复基于元素的equals()方法而非==运算符
    • 示例:可以添加多个new String("hello")对象到List中
  3. 可null性(Nullable):

    • 支持存储null值
    • 允许存储多个null值
    • 示例:
      list.add(null);
      list.add(null); // 允许
      

    • 注意:某些具体实现可能有特殊限制,如ConcurrentHashMap不允许null值

常见实现类包括ArrayList(基于动态数组)、LinkedList(基于双向链表)、Vector(线程安全版本)等。List接口还提供了丰富的工具方法,如sort()、subList()、indexOf()等,使其成为处理有序集合的首选接口。

二、List 接口的常用实现类

List接口是Java集合框架中最重要的接口之一,它有多种实现类,各有特点和适用场景。以下是三种最常用的实现类的详细说明:

1. ArrayList(基于动态数组的实现)

底层数据结构

  • 使用Object[]数组作为存储结构
  • 默认初始容量为10(可通过构造函数指定其他初始容量)
  • 当元素数量达到当前容量时,会自动扩容(每次扩容为原容量的1.5倍)
  • 扩容时调用Arrays.copyOf()方法创建新数组并复制元素

性能特点

  • 随机访问效率极高:通过索引直接定位元素,时间复杂度O(1)
    • 示例:list.get(100)可以立即返回第101个元素
  • 插入/删除效率较低:需要移动后续元素,时间复杂度O(n)
    • 示例:在ArrayList开头插入元素需要移动所有现有元素

适用场景

  • 需要频繁读取数据的场合(如数据展示列表)
  • 元素数量相对固定或增长缓慢的情况
  • 需要快速随机访问元素的场合

使用示例

List<String> names = new ArrayList<>();
names.add("Alice"); // 添加元素
names.add(0, "Bob"); // 在指定位置插入(效率较低)
String name = names.get(1); // 快速随机访问

2. LinkedList(基于双向链表的实现)

底层数据结构

  • 使用双向链表存储元素
  • 每个节点(Node)包含:
    • 前驱指针(prev)
    • 后继指针(next)
    • 元素值(item)
  • 维护了头节点(first)和尾节点(last)

性能特点

  • 插入/删除效率高:只需修改指针指向,时间复杂度O(1)
    • 前提是已定位到操作节点(如果未定位,定位时间为O(n))
  • 随机访问效率低:需要从头或尾遍历链表,时间复杂度O(n)

适用场景

  • 需要频繁插入/删除元素的场合
  • 实现队列(Queue)或栈(Stack)结构
  • 需要实现LRU缓存等需要快速头尾操作的场景

使用示例

List<Integer> numbers = new LinkedList<>();
numbers.add(10); // 添加到末尾
numbers.addFirst(5); // 添加到头部(高效)
numbers.removeLast(); // 移除最后一个元素(高效)

3. Vector(线程安全的数组实现)

底层数据结构

  • 与ArrayList类似,基于Object[]数组
  • 方法都加了synchronized关键字,保证线程安全
  • 默认扩容机制为原来的2倍(不同于ArrayList的1.5倍)

性能特点

  • 线程安全但性能较差:由于同步锁的开销
  • 随机访问效率与ArrayList相同
  • 插入/删除效率比ArrayList更低(因为需要同步)

适用场景

  • 需要线程安全但并发不高的场景
  • 遗留系统维护(新代码建议使用CopyOnWriteArrayList)

当前状态

  • 已被Java 5引入的CopyOnWriteArrayList替代
  • CopyOnWriteArrayList采用写时复制技术,读操作完全无锁,更适合高并发场景

使用示例

List<String> safeList = new Vector<>();
safeList.add("item"); // 线程安全的操作
String item = safeList.get(0); // 线程安全的读取

三、List 接口的核心方法

1. 基础增删改查操作

添加元素

  • add(E e):在列表末尾添加指定元素。例如:

    List<String> list = new ArrayList<>();
    list.add("Apple");  // ["Apple"]
    list.add("Banana"); // ["Apple", "Banana"]
    

  • add(int index, E e):在指定索引位置插入元素,原位置及后续元素自动后移。例如:

    list.add(1, "Orange"); // ["Apple", "Orange", "Banana"]
    

删除元素

  • remove(int index):删除指定索引位置的元素并返回该元素。例如:

    String removed = list.remove(1); // 返回"Orange",列表变为["Apple", "Banana"]
    

  • remove(Object o):删除列表中第一个与指定对象匹配的元素,返回是否删除成功。例如:

    boolean isRemoved = list.remove("Banana"); // 返回true,列表变为["Apple"]
    

修改和获取元素

  • set(int index, E e):替换指定索引位置的元素并返回旧元素。例如:

    String old = list.set(0, "Grape"); // 返回"Apple",列表变为["Grape"]
    

  • get(int index):获取指定索引位置的元素。例如:

    String fruit = list.get(0); // 返回"Grape"
    

2. 集合操作

批量操作

  • addAll(Collection<? extends E> c):将指定集合中的所有元素添加到当前列表末尾。例如:

    List<String> fruits = Arrays.asList("Peach", "Pear");
    list.addAll(fruits); // 列表变为["Grape", "Peach", "Pear"]
    

  • clear():清空列表中的所有元素。例如:

    list.clear(); // 列表变为空列表[]
    

查询操作

  • contains(Object o):判断列表是否包含指定元素,基于元素的equals()方法比较。例如:

    boolean hasApple = list.contains("Apple"); // 返回false
    

  • indexOf(Object o):返回指定元素在列表中第一次出现的索引,不存在则返回-1。例如:

    list.add("Apple");
    list.add("Apple");
    int firstIndex = list.indexOf("Apple"); // 返回0
    

  • lastIndexOf(Object o):返回指定元素在列表中最后一次出现的索引。例如:

    int lastIndex = list.lastIndexOf("Apple"); // 返回1
    

子列表操作

  • subList(int fromIndex, int toIndex):返回指定索引范围的子列表视图。注意子列表与原列表共享数据,对子列表的修改会影响原列表。例如:
    List<String> sub = list.subList(0, 2); // 返回["Apple", "Apple"]
    sub.set(1, "Banana"); // 原列表变为["Apple", "Banana"]
    

3. 其他常用方法

基本信息查询

  • size():返回列表中元素的数量。例如:

    int count = list.size(); // 返回2
    

  • isEmpty():判断列表是否为空。例如:

    boolean empty = list.isEmpty(); // 返回false
    

数组转换

  • toArray():将列表转换为Object数组。例如:

    Object[] array = list.toArray(); // 返回["Apple", "Banana"]对应的数组
    

  • toArray(T[] a):指定类型的数组转换(更常用)。例如:

    String[] strArray = list.toArray(new String[0]);
    

四、List 的遍历方式

1. 普通for循环(基于索引)

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

特点与注意事项

  • 显式使用索引访问集合元素
  • ArrayList等基于数组实现的集合效率较高(O(1)随机访问)
  • LinkedList使用此方式效率极低,因为每次get(i)都需要从头遍历链表(时间复杂度O(n))
  • 适合需要根据索引操作元素的场景
  • 示例:需要在遍历时同时处理元素和索引位置

2. 增强for循环(foreach)

for (E element : list) {
    System.out.println(element);
}

优势

  • 语法简洁,可读性高
  • 编译器自动转换为迭代器实现
  • 适用于所有实现了Iterable接口的集合类

劣势

  • 无法直接获取当前元素的索引
  • 不能在遍历中修改集合结构(会抛出ConcurrentModificationException)
  • 示例:适合只需要读取元素而不需要修改集合的场景

3. 迭代器(Iterator)

Iterator<E> iterator = list.iterator();
while (iterator.hasNext()) {
    E element = iterator.next();
    if (条件) {
        iterator.remove(); // 支持安全删除(不会触发并发修改异常)
    }
}

特点

  • 提供标准的遍历接口
  • 支持在遍历过程中安全删除当前元素
  • 适用于所有Collection类型的集合
  • 只能单向遍历(从前往后)
  • 示例:需要过滤集合中某些元素的场景

4. ListIterator(支持双向遍历)

ListIterator<E> listIterator = list.listIterator();
while (listIterator.hasNext()) { // 正向遍历
    E element = listIterator.next();
}

while (listIterator.hasPrevious()) { // 反向遍历
    E element = listIterator.previous();
}

特点

  • 继承自Iterator,具备所有迭代器功能
  • 支持双向遍历(正向和反向)
  • 支持在遍历过程中添加、修改和删除元素
  • 可以获取当前元素的索引位置(nextIndex()/previousIndex())
  • 仅List接口的实现类支持
  • 示例:需要双向遍历链表或需要根据位置修改元素的场景

五、注意事项与避坑指南

1. 并发修改异常(ConcurrentModificationException)

触发场景:当使用增强 for 循环或迭代器遍历集合时,如果通过集合自身的方法(如 add()、remove())直接修改集合结构,就会抛出此异常。例如:

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
for (String s : list) {
    if (s.equals("b")) {
        list.remove(s); // 抛出 ConcurrentModificationException
    }
}

解决方法

  • 使用迭代器的 remove() 方法删除元素:
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        if (it.next().equals("b")) {
            it.remove(); // 正确方式
        }
    }
    

  • 使用 CopyOnWriteArrayList(适合读多写少场景,修改时会复制底层数组)
  • 使用 Java 8+ 的 removeIf() 方法:
    list.removeIf(s -> s.equals("b"));
    

2. 索引越界异常(IndexOutOfBoundsException)

触发场景:在调用 get(index)、add(index, e)、set(index, e) 等方法时,当 index 小于 0 或大于等于集合的 size() 时抛出。例如:

List<String> list = new ArrayList<>();
list.get(0); // 抛出 IndexOutOfBoundsException

避免方式

  • 操作前检查索引合法性:
    if (index >= 0 && index < list.size()) {
        list.get(index);
    }
    

  • 使用安全的访问方法:
    // Java 8+ 可以使用 Optional 避免 NPE
    Optional.ofNullable(list)
            .filter(l -> index >= 0 && index < l.size())
            .map(l -> l.get(index));
    

3. equals() 与 hashCode() 的重写

重要原则

  • List 的 contains()、indexOf()、remove(Object) 等方法依赖元素的 equals() 方法判断
  • 若元素是自定义对象,必须重写 equals() 方法
  • 建议同时重写 hashCode() 方法,遵循"相等对象必须有相等哈希码"原则

示例

class Person {
    private String name;
    private int age;
    
    // 省略构造方法等
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

4. subList 的注意事项

问题

  • subList 返回的是原列表的视图,而非独立的集合
  • 修改子列表会影响原列表
  • 原列表结构修改后(如 add/remove),子列表操作会抛 ConcurrentModificationException

示例

List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
List<String> sub = list.subList(1, 3); // ["b", "c"]
sub.set(0, "B"); // 原列表变为 ["a", "B", "c", "d"]
list.add("e"); // 此时再操作 sub 会抛异常

正确用法

// 如需独立子列表,创建新集合
List<String> independentSub = new ArrayList<>(list.subList(1, 3));

5. ArrayList 的初始容量优化

性能建议

  • ArrayList 默认初始容量为 10,扩容时会创建新数组并复制元素
  • 若已知元素数量,初始化时指定容量可减少扩容次数
  • 预估容量公式:初始容量 = 预估元素数量 × 1.5(考虑扩容因子)

示例

// 已知大约有100个元素
List<String> list = new ArrayList<>(150); // 100*1.5

// 或者根据已有集合大小初始化
List<String> anotherList = new ArrayList<>(existingList.size());

6. LinkedList 的遍历效率

性能问题

  • LinkedList 的 get(index) 方法时间复杂度为 O(n)
  • 频繁调用 get(index) 遍历会导致性能问题

优化方案

  • 使用迭代器(推荐):
    for (Iterator<String> it = list.iterator(); it.hasNext(); ) {
        String s = it.next();
        // 处理元素
    }
    

  • 使用增强 for 循环(底层也是迭代器):
    for (String s : list) {
        // 处理元素
    }
    

  • 使用 Java 8+ 的 forEach:
    list.forEach(s -> {
        // 处理元素
    });
    

性能对比

  • 对于 LinkedList,使用 get(index) 遍历 10000 个元素可能需要数百毫秒
  • 使用迭代器方式仅需几毫秒


网站公告

今日签到

点亮在社区的每一天
去签到