1. 概述
在 Java 中遍历集合的方式有很多种。本文重点对比两种看起来很像的方法:Collection.forEach()
和 Collection.stream().forEach()
。
在大多数情况下,两者的行为看起来是一致的,但它们之间还是存在一些细微但重要的差异,尤其是在执行顺序、并发修改和自定义迭代器方面。
2. 一个简单的 List 示例
我们先定义一个用于遍历的列表:
List<String> list = Arrays.asList("A", "B", "C", "D");
最传统的方式是使用增强型 for 循环:
for (String s : list) {
// do something with s
}
如果你偏好函数式风格,也可以使用 forEach()
:
✅ 直接在集合上调用:
Consumer<String> consumer = s -> System.out.println(s);
list.forEach(consumer);
✅ 或者通过 Stream 调用:
list.stream().forEach(consumer);
两种方式都会输出:
A
B
C
D
在这个简单场景下,两者的行为没有区别。
3. 执行顺序
这是两者之间最显著的区别之一:
Collection.forEach()
会使用集合的迭代器(如果有自定义的),所以执行顺序是确定的;Stream.forEach()
的执行顺序是不确定的,尤其是在并行流中。
3.1 并行流中的行为差异
当我们使用并行流时,多个线程会并发执行,顺序自然无法保证。
示例:
list.forEach(System.out::print); // 顺序输出:ABCD
System.out.print(" ");
list.parallelStream().forEach(System.out::print); // 输出顺序每次可能不同
输出示例 1:
ABCD CDBA
输出示例 2:
ABCD DBCA
可以看到,forEach()
保持插入顺序,而 parallelStream().forEach()
每次结果不同。
3.2 自定义迭代器的影响
我们定义一个逆序迭代的 List:
class ReverseList extends ArrayList<String> {
@Override
public Iterator<String> iterator() {
int startIndex = this.size() - 1;
List<String> list = this;
return new Iterator<>() {
private int currentIndex = startIndex;
@Override
public boolean hasNext() {
return currentIndex >= 0;
}
@Override
public String next() {
String next = list.get(currentIndex);
currentIndex--;
return next;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
}
然后测试:
List<String> myList = new ReverseList();
myList.addAll(list);
myList.forEach(System.out::print); // 输出:DCBA
myList.stream().forEach(System.out::print); // 输出:ABCD
⚠️ 原因:forEach()
使用了自定义的逆序迭代器,而 stream().forEach()
忽略了迭代器,直接按索引顺序处理元素。
4. 集合修改行为
很多集合(如 ArrayList
、HashSet
)在遍历时不允许结构性修改(添加或删除元素),否则会抛出 ConcurrentModificationException
异常。
两者的区别在于:
Collection.forEach()
是 fail-fast,一旦发现修改会立即抛异常;Stream.forEach()
的异常检查发生在流处理的后期,所以会继续处理部分元素后再抛异常。
4.1 删除元素的示例
定义一个删除操作:
Consumer<String> removeElement = s -> {
System.out.println(s + " " + list.size());
if (s != null && s.equals("A")) {
list.remove("D");
}
};
✅ 使用 forEach()
:
list.forEach(removeElement);
输出:
A 4
Exception in thread "main" java.util.ConcurrentModificationException
⚠️ 只处理了第一个元素就抛出异常。
✅ 使用 stream().forEach()
:
list.stream().forEach(removeElement);
输出:
A 4
B 3
C 3
null 3
Exception in thread "main" java.util.ConcurrentModificationException
⚠️ 多处理了几个元素才抛异常。
✅ 注意:**Java 不保证一定会抛出 ConcurrentModificationException
**,所以永远不要依赖这个异常来编写逻辑。
4.2 修改元素的行为
我们可以在遍历时修改元素:
list.forEach(e -> {
list.set(3, "E");
});
使用 forEach()
或 stream().forEach()
都不会报错,但在流处理中,Java 要求操作必须是 non-interfering(无干扰),也就是说在流处理期间不应该修改源集合。
⚠️ 修改流中的元素可能会导致不可预测的行为,尤其是在并行流中。
5. 总结
特性 | Collection.forEach() |
Stream.forEach() |
---|---|---|
执行顺序 | 使用迭代器,顺序确定 | 顺序不确定,尤其在并行流中 |
自定义迭代器 | 会使用 | 忽略 |
修改集合 | fail-fast,立即抛异常 | 后期才检查,可能处理部分元素后抛异常 |
修改元素 | 允许,但不推荐 | 不符合流设计原则,可能导致问题 |
✅ 建议:
- 如果你只是想遍历集合,**优先使用
Collection.forEach()
**; - 如果你需要流式处理(如 filter/map/reduce),才使用
Stream.forEach()
。
⚠️ 不要依赖这两种方法的异常行为或执行顺序来编写关键逻辑。
完整代码可在 GitHub 上查看。