Java 集合体系详解:List 接口、遍历方式及底层原理
一、Collection 接口核心概览
在 Java 集合框架中,Collection 是单列集合的顶层接口。根据数据特性不同,主要分为两大体系:List(允许重复、有序、带索引)和 Set(禁止重复、无序、无索引)。
以下是 Collection 接口中最常用的功能方法:
| API 签名 | 功能说明 |
|---|---|
boolean add(E e) |
向容器内追加新元素 |
boolean remove(Object o) |
移除指定的对象实例 |
void clear() |
清空容器内的所有数据 |
boolean contains(Object o) |
检索特定元素是否存在 |
int size() |
获取当前存储元素的总数 |
注意:在使用 contains 或 remove 比较自定义对象时,建议重写类的 equals 方法,否则默认会比较内存地址。
二、集合遍历的多种策略
对集合数据的访问有多种模式,选择哪种取决于具体场景。
2.1 使用迭代器 (Iterator)
这是最基础的遍历方式,适用于所有实现了 Collection 接口的类。其核心流程包含两个步骤:检查是否有下一项 (hasNext) 以及获取该项 (next)。
package com.example.traversal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class BasicIteration {
public static void main(String[] args) {
// 初始化容器
Collection<String> repository = new ArrayList<>();
repository.addAll(java.util.Arrays.asList("ProjectA", "ProjectB", "ProjectC"));
// 获取游标对象
Iterator<String> cursor = repository.iterator();
// 循环读取
while (cursor.hasNext()) {
String currentTask = cursor.next();
System.out.println("处理任务:" + currentTask);
}
// 安全性提示:在遍历过程中直接调用集合的 remove 会抛出异常
// 必须通过迭代器自带的 remove() 方法来删除当前节点
}
}
如果在遍历过程中需要动态删除元素,必须调用 iterator.remove(),而不是集合对象的 remove()。
public class SafeRemoval {
public static void main(String[] args) {
ArrayList<Integer> nums = new ArrayList<>();
for(int i=0; i<5; i++) nums.add(i);
Iterator<Integer> walker = nums.iterator();
while(walker.hasNext()){
int val = walker.next();
if(val % 2 == 0) { // 移除偶数
walker.remove();
}
}
System.out.println(nums);
}
}
2.2 增强型 for 循环
JDK 5 引入的语法糖,底层依然依赖迭代器实现,但代码更简洁,适合只读遍历。
public class ForEachSyntax {
public static void main(String[] args) {
ArrayList<String> products = new ArrayList<>();
products.add("Laptop");
products.add("Mouse");
for (String item : products) {
System.out.println("库存物品:" + item);
}
}
}
2.3 Lambda 表达式与函数式遍历
利用 forEach 方法配合 Consumer 接口,可以以声明式风格执行逻辑。
public class FunctionalTraversal {
public static void main(String[] args) {
Collection<String> users = new ArrayList<>();
users.add("Admin");
users.add("User1");
// 传统匿名内部类写法
/*users.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});*/
// Lambda 简化写法
users.forEach(name -> System.out.println("Hello, " + name));
}
}
三、List 接口的高级操作
List 继承自 Collection,增加了基于索引的控制能力。主要特征包括:有序性、元素可重复、支持随机访问。
| 方法 | 行为描述 |
|---|---|
void add(int index, E element) |
在指定下标位置插入新元素 |
E get(int index) |
获取指定下标的元素 |
E set(int index, E element) |
替换指定下标的元素,返回旧值 |
E remove(int index) |
删除指定下标的元素,返回被删值 |
public class ListIndexOps {
public static void main(String[] args) {
List<String> tasks = new ArrayList<>();
tasks.add("Task 1");
tasks.add("Task 2");
// 插入到索引 0 处,原有元素后移
tasks.add(0, "Priority Task");
// 修改索引 1 处的内容
String oldVal = tasks.set(1, "Modified Task");
// 删除索引 2 处的内容
tasks.remove(2);
System.out.println(tasks.get(0));
}
}
重载陷阱说明: remove 方法存在重载版本,一个是接收 Object(删除指定值的第一个匹配项),另一个接收 int(删除指定位置的元素)。若传入的是 Integer 对象,会被当做 Object 处理;若传入字面量数字,可能被当做索引处理,需谨慎区分。
四、数据结构视角的实现差异
4.1 数组链表对比理论
- 栈 (Stack):后进先出 (LIFO)。
- 队列 (Queue):先进先出 (FIFO)。
- 数组 (Array):内存连续,查询效率高 (O(1)),但在中间插入/删除性能差,需要移动大量数据。
- 链表 (Linked List):内存不连续,查询慢 (O(n)),但增删操作只需修改指针指向,效率较高。
4.2 ArrayList 实现机制
ArrayList 基于动态数组实现。初始创建时,内部维护一个长度为 10 的数组(elementData)和一个计数器(size)。
- 扩容策略:当空间不足时,会自动分配新数组。新容量通常是旧容量的 1.5 倍(
oldCapacity + (oldCapacity >> 1))。 - 极端情况:如果需要一次性添加大量元素导致 1.5 倍仍不足,则直接按需求容量分配。
- 代价:每次扩容都涉及
System.arraycopy拷贝,频繁扩容会影响性能。
4.3 LinkedList 实现机制
LinkedList 基于双向链表实现。每个节点包含三个部分:前驱节点引用、数据域、后继节点引用。
- 头尾指针:内部维护
first和last指针指向首尾节点。 - 插入过程:新增节点只需调整相邻节点的指针引用,无需移动数据。
- 特有方法:由于链表结构,它提供了如
addFirst,addLast,removeFirst等便捷方法,使其也能作为队列或双端队列使用。
// LinkedList 独有 API 示例
public class DqueuOperations {
public static void main(String[] args) {
LinkedList<String> deque = new LinkedList<>();
deque.addFirst("Header");
deque.addLast("Footer");
System.out.println(deque.getFirst());
deque.removeLast();
}
}
4.4 迭代器源码简析
无论是 ArrayList 还是 LinkedList,其迭代器均遵循 Iterator 接口规范。源码层面通常维护了一个期望修改计数(expectedModCount)和当前游标位置(cursor)。若在遍历时集合发生结构性变化且非由迭代器自身引起,下次调用时便会抛出 ConcurrentModificationException,以保障一致性。