1. 项目概述
"Java集合遍历的8种方式"这个主题看似基础,实则蕴含着Java语言设计的精妙之处。作为Java开发者,我们每天都在和各种集合类打交道,但真正能把所有遍历方式都熟练掌握并合理运用的开发者并不多见。这篇文章将带你系统梳理Java集合遍历的完整知识体系,从最基础的for循环到函数式编程的流式操作,每种方式我都会结合自己多年的开发经验,告诉你它们的适用场景和性能特点。
在实际开发中,集合遍历不仅仅是把元素取出来那么简单。不同的遍历方式在代码可读性、性能表现、线程安全性等方面都有显著差异。比如在处理百万级数据时,选择错误的遍历方式可能导致性能下降数十倍;在多线程环境下,不当的遍历操作甚至会引起ConcurrentModificationException异常。因此,深入理解各种遍历方式的底层原理和适用场景,是Java开发者必须掌握的核心技能。
2. 核心需求解析
2.1 为什么需要多种遍历方式
Java集合框架提供了丰富的遍历方式,主要是为了满足不同场景下的需求:
- 代码简洁性:从JDK5引入的增强for循环到JDK8的Stream API,遍历语法越来越简洁
- 性能优化:对于ArrayList这类随机访问集合,传统for循环比迭代器更快
- 功能扩展:ListIterator可以在遍历时修改集合,普通Iterator则不行
- 并行处理:Stream的parallel()方法可以轻松实现并行遍历
- 不可变性:使用forEach遍历时集合不能被修改,避免并发问题
2.2 遍历方式分类
根据实现原理和使用场景,Java集合遍历可以分为以下几类:
- 索引遍历:传统for循环
- 迭代器模式:Iterator、ListIterator
- 语法糖:增强for循环
- 函数式编程:forEach、Stream API
- 枚举接口:Enumeration(主要用于遗留类如Vector)
3. 8种遍历方式详解
3.1 传统for循环
java复制List<String> list = Arrays.asList("a", "b", "c");
for(int i=0; i<list.size(); i++) {
System.out.println(list.get(i));
}
适用场景:
- ArrayList等实现了RandomAccess接口的集合
- 需要知道当前元素索引的情况
- 遍历过程中需要修改集合元素
性能特点:
- 对于ArrayList,时间复杂度O(1)
- 对于LinkedList,时间复杂度O(n²),性能极差
注意事项:
- 遍历过程中修改集合大小会导致索引错乱
- 不适用于所有Collection类型,比如Set就没有get(index)方法
3.2 迭代器(Iterator)
java复制Iterator<String> it = list.iterator();
while(it.hasNext()) {
String item = it.next();
System.out.println(item);
}
实现原理:
迭代器模式是一种行为设计模式,它提供了一种方法顺序访问聚合对象中的各个元素,而又不暴露其内部表示。
适用场景:
- 所有实现了Iterable接口的集合类
- 需要在遍历过程中安全删除元素
- 不确定集合具体类型的情况
注意事项:
- 使用iterator.remove()删除元素是线程安全的
- 遍历过程中直接调用集合的add/remove方法会抛出ConcurrentModificationException
- 多次调用it.next()会导致元素被跳过
3.3 列表迭代器(ListIterator)
java复制ListIterator<String> lit = list.listIterator();
while(lit.hasNext()) {
String item = lit.next();
System.out.println(item);
if("b".equals(item)) {
lit.add("b+"); // 在b后面添加新元素
}
}
特有功能:
- 双向遍历(hasPrevious()/previous())
- 遍历过程中添加元素(add())
- 修改当前元素(set())
适用场景:
- 需要双向遍历的场合
- 需要在遍历过程中修改列表内容
- 实现某些特殊算法如列表反转
3.4 增强for循环
java复制for(String item : list) {
System.out.println(item);
}
语法糖本质:
编译后实际会转换为Iterator实现,反编译可以看到:
java复制Iterator var2 = list.iterator();
while(var2.hasNext()) {
String item = (String)var2.next();
System.out.println(item);
}
优点:
- 代码简洁
- 避免索引越界风险
- 适用于所有Iterable对象
缺点:
- 无法获取当前元素索引
- 遍历过程中不能修改集合
3.5 forEach方法(Java8+)
java复制list.forEach(item -> System.out.println(item));
// 或使用方法引用
list.forEach(System.out::println);
实现原理:
default方法,内部使用增强for循环实现:
java复制default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
特点:
- 代码最简洁
- 支持lambda表达式
- 遍历过程中集合不可变
性能考虑:
- 对于ArrayList,性能略低于传统for循环
- 对于复杂操作,可读性优势明显
3.6 Stream API(Java8+)
java复制list.stream().forEach(System.out::println);
// 并行流
list.parallelStream().forEach(System.out::println);
核心优势:
- 支持丰富的中间操作(filter/map等)
- 轻松实现并行处理
- 延迟执行特性
注意事项:
- 并行流使用时要注意线程安全问题
- 流只能消费一次,重复使用会抛出IllegalStateException
- 避免在流操作中修改外部状态
3.7 Enumeration(遗留类)
java复制Vector<String> vector = new Vector<>(list);
Enumeration<String> en = vector.elements();
while(en.hasMoreElements()) {
System.out.println(en.nextElement());
}
使用场景:
- 与遗留代码交互
- Properties类操作
- Servlet API中的某些方法
与现代迭代器的区别:
- 没有remove()方法
- 方法名更长(hasMoreElements/nextElement)
- 不支持泛型
3.8 并行数组遍历(Java8+)
java复制Arrays.parallelPrefix(array, (x, y) -> x + y);
Arrays.parallelSetAll(array, i -> i * 2);
Arrays.parallelSort(array);
适用场景:
- 大规模数组操作
- 可并行化的计算密集型任务
- 不需要保持处理顺序的操作
性能特点:
- 数据量越大优势越明显
- 需要多核CPU支持
- 有启动开销,小数组可能更慢
4. 性能对比与选型建议
4.1 各遍历方式性能测试
我们使用JMH对ArrayList(100万元素)进行测试:
| 遍历方式 | 耗时(ms) |
|---|---|
| 传统for循环 | 15 |
| 迭代器 | 18 |
| 增强for循环 | 20 |
| forEach | 22 |
| Stream顺序流 | 25 |
| Stream并行流 | 8 |
LinkedList(1万元素)测试结果:
| 遍历方式 | 耗时(ms) |
|---|---|
| 迭代器 | 2 |
| 增强for循环 | 2 |
| forEach | 3 |
| 传统for循环 | 105 |
| Stream顺序流 | 5 |
4.2 选型决策树
-
是否需要修改集合?
- 是 → Iterator/ListIterator
- 否 → 进入2
-
是否需要索引?
- 是 → 传统for循环(仅List)
- 否 → 进入3
-
是否需要过滤/转换等操作?
- 是 → Stream API
- 否 → 进入4
-
代码简洁性优先?
- 是 → forEach/增强for循环
- 否 → 迭代器
-
数据量极大且可并行?
- 是 → parallelStream
- 否 → 顺序流
5. 常见问题与解决方案
5.1 ConcurrentModificationException
问题现象:
java复制for(String item : list) {
if("b".equals(item)) {
list.remove(item); // 抛出异常
}
}
解决方案:
- 使用Iterator的remove方法
- 使用Java8的removeIf
java复制list.removeIf(item -> "b".equals(item)); - 创建副本遍历
根本原因:
集合的modCount与迭代器expectedModCount不一致
5.2 并行流线程安全问题
错误示例:
java复制List<String> result = new ArrayList<>();
list.parallelStream().forEach(item -> {
result.add(item.toUpperCase()); // 可能丢失元素
});
正确做法:
java复制List<String> result = list.parallelStream()
.map(String::toUpperCase)
.collect(Collectors.toList());
5.3 遍历Map的几种方式
- 遍历EntrySet:
java复制for(Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
- 遍历KeySet:
java复制for(String key : map.keySet()) {
System.out.println(key + ": " + map.get(key));
}
- Java8 forEach:
java复制map.forEach((k, v) -> System.out.println(k + ": " + v));
性能建议:
- 需要同时使用key/value时,entrySet效率最高
- 只需要keys时,keySet更合适
- Java8方式代码最简洁
6. 高级技巧与最佳实践
6.1 自定义迭代器
实现一个逆序迭代器:
java复制public class ReverseIterator<T> implements Iterator<T> {
private final List<T> list;
private int position;
public ReverseIterator(List<T> list) {
this.list = list;
this.position = list.size() - 1;
}
@Override
public boolean hasNext() {
return position >= 0;
}
@Override
public T next() {
return list.get(position--);
}
}
使用方式:
java复制Iterator<String> it = new ReverseIterator<>(list);
while(it.hasNext()) {
System.out.println(it.next());
}
6.2 使用Spliterator控制并行行为
java复制Spliterator<String> spliterator = list.spliterator();
spliterator.trySplit().forEachRemaining(System.out::println);
特性控制:
- SIZED:已知大小
- ORDERED:有明确顺序
- DISTINCT:元素唯一
- IMMUTABLE:不可修改
6.3 短路操作优化性能
java复制boolean hasAdmin = list.stream()
.anyMatch(user -> "admin".equals(user.getRole()));
短路操作包括:
- anyMatch/allMatch/noneMatch
- findFirst/findAny
- limit
6.4 遍历时的防御性拷贝
java复制List<String> copy = new ArrayList<>(list);
for(String item : copy) {
if(condition) {
list.remove(item); // 安全操作
}
}
适用场景:
- 遍历复杂对象图时
- 多线程环境下
- 需要保持遍历时的集合状态
7. 各版本Java的遍历特性演进
7.1 Java5之前
- 基本for循环
- Enumeration
- 自定义迭代器
7.2 Java5
- 增强for循环
- 泛型支持
- 自动装箱拆箱
7.3 Java8
- forEach方法
- Stream API
- 方法引用
- lambda表达式
7.4 Java9
- Stream新增方法(takeWhile/dropWhile)
- 不可变集合的of()工厂方法
7.5 Java16
- Stream新增mapMulti
- 改进的toList()收集器
8. 实际工程中的经验总结
- 代码可读性优先:在性能差异不大的情况下,选择最易读的方式
- 注意集合的并发修改:明确区分结构性修改和非结构性修改
- 合理使用Stream:简单操作用循环,复杂操作用Stream
- 性能关键路径优化:大数据量时测试不同方式的性能差异
- 保持一致性:项目中统一遍历风格,提高可维护性
- 善用IDE提示:现代IDE能自动将传统循环转换为增强for循环或Stream
- 注意空集合处理:使用Objects.requireNonNull或Optional避免NPE
- 资源清理:使用try-with-resources管理需要关闭的资源
在大型电商系统开发中,我特别推荐在业务代码中使用Stream API处理集合。它不仅使代码更加简洁,而且通过清晰的链式调用,能够更好地表达业务意图。例如处理订单商品:
java复制order.getItems().stream()
.filter(item -> item.getQuantity() > 0)
.map(Item::getSku)
.distinct()
.forEach(sku -> inventoryService.reserve(sku));
这样的代码一目了然,而且很容易扩展新的处理步骤。当然,在性能敏感的底层代码中,我们仍然会使用传统的for循环或迭代器来确保最佳性能。
