一、List 接口的定义与特性
List接口位于java.util包下,是Java集合框架中最常用的接口之一,定义为:
public interface List<E> extends Collection<E>
其核心特性如下:
有序性(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
可重复性(Allow Duplicates):
- 允许存储多个equals()方法返回true的元素
- 判断重复基于元素的equals()方法而非==运算符
- 示例:可以添加多个new String("hello")对象到List中
可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 个元素可能需要数百毫秒
- 使用迭代器方式仅需几毫秒