Java 8 forEach 中断循环构建 List 方案
2025-03-12 00:01:19
Java 8 中 Foreach 循环构建 List 并根据条件中断
有些时候,我们需要遍历一个集合,在循环内部构建一个新的 List,并且当满足特定条件时,需要中断这个循环。原有的 forEach
方法不支持直接的 break
操作,这会带来一些麻烦。 比如,像下面这样的代码:
List<Ctry> result = new ArrayList<Ctry>();
activeRow.forEach(obj -> {
if (CollectionUtils.isEmpty(result)) {
result.addAll(this.findCtry(obj,true));
} else {
return; // 这实际上只是跳过当前迭代,不是 break!
}
});
这里想实现的效果是,只要 result
不为空,就跳出整个循环。但 forEach
里面的 return
只能结束当前这 一次 循环,并不能跳出整个 forEach
。这篇博客文章会提供一些方法,帮你解决这个问题。
问题根源:forEach 的设计
forEach
是 Java 8 引入的 Stream API 的一部分。它本质上是一个 消费型 操作(Consumer)。你可以把它想象成一个接收器,每次接收集合中的一个元素,然后做一些事情(比如,运行你提供的 lambda 表达式)。forEach
的设计目标是 遍历 整个集合,它 没有 提供中断循环的机制。
解决方案
有好几种方式可以处理这个问题。咱们一个个来看。
1. 使用传统的 for
循环
老办法有时候最管用!既然 forEach
不方便中断,我们干脆回归传统的 for
循环:
List<Ctry> result = new ArrayList<Ctry>();
for (YourObjectType obj : activeRow) {
if (CollectionUtils.isEmpty(result)) {
result.addAll(this.findCtry(obj, true));
} else {
break; // 直接中断循环!
}
}
原理: 老式的 for
循环允许你完全控制循环过程,包括使用 break
语句直接跳出。
代码示例: 见上文。
安全建议: 没什么特别要注意的,for
循环本身很安全。
进阶:
在有些情况下如果我们知道List中的元素的索引,并且对元素的索引有判断条件可以优先使用传统的带索引的for
循环。
2. 使用 anyMatch()
+ !isEmpty()
anyMatch()
是 Stream API 提供的另一个方法。 它可以用来检查流中 是否 存在至少一个元素匹配某个条件。我们可以利用这个特性来间接实现中断。
List<Ctry> result = new ArrayList<Ctry>();
activeRow.stream().anyMatch(obj -> {
if (result.isEmpty()) {
result.addAll(this.findCtry(obj, true));
return false; //继续
} else
return true;//找到了
});
原理:
- 将
activeRow
转换成一个 Stream。 - 调用
anyMatch()
方法,传入一个 Predicate(判断条件)。 - 如果
result
为空,就调用findCtry
并将结果添加到result
中,并且由于没有break
跳出条件所以返回false
, 使anyMatch
能继续检查流的元素。 - 如果
result
不为空,就说明条件已经满足,由于我们已经将findCtry
方法的结果添加到List中,所以为了达到跳出循环的目的就返回true
, 表示已经找到元素。 这会让anyMatch()
立即 停止处理后续元素(短路行为)。
代码示例: 见上文。
安全建议: 这种方式仍然涉及到对 result
的修改,,如果有并发操作,可能需要额外的同步措施。
进阶:
这里实际上不是使用了break
语句,而是利用了Stream 的anyMatch()
方法的特性进行模拟的跳出循环。如果你的activeRow
很大,由于提前中断,这种方法理论上效率会高一些,因为不会处理 所有 元素。
3. 使用 takeWhile()
(Java 9+)
如果你用的是 Java 9 或更高版本,可以试试 takeWhile()
方法。它会从流的 开头 获取元素,直到遇到 第一个 不满足条件的元素为止。
List<Ctry> result = new ArrayList<Ctry>();
activeRow.stream()
.takeWhile(obj -> result.isEmpty())
.forEach(obj -> result.addAll(this.findCtry(obj, true)));
原理:
takeWhile()
持续获取元素,只要result.isEmpty()
返回true
。- 一旦
result.isEmpty()
返回false
(也就是result
不为空了),takeWhile()
就会停止获取元素,后续的forEach
也就不会执行了。
代码示例: 见上文。
安全建议: 与前面一样,并发修改 result
时需要注意。
进阶:
和anyMatch()
方法一样利用了Stream的特性来实现跳出循环,如果只需要集合的前面的部分元素,并且可以基于一个明确的条件来判断何时停止,那么takeWhile()
是一种简洁且具有可读性的方式。
4. 自定义 Collector (进阶)
对于更复杂的情况,你可以自定义一个 Collector。Collector 负责将 Stream 中的元素收集到一个结果容器中(比如 List、Set 等)。
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.Set;
import java.util.EnumSet;
import org.springframework.util.CollectionUtils;
public class BreakableCollector {
public static <T> Collector<T, ?, List<T>> toListBreaking(Function<List<T>, Boolean> breakCondition, BiConsumer<List<T>,T> action) {
return new Collector<T, List<T>, List<T>>() {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> {
if(!breakCondition.apply(list)) {
action.accept(list,item);
}
};
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
if(!breakCondition.apply(list1)){
list1.addAll(list2);
}
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return list -> list;
}
@Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.IDENTITY_FINISH);
}
};
}
}
使用示例:
List<Ctry> result = activeRow.stream()
.collect(BreakableCollector.toListBreaking(
list -> !CollectionUtils.isEmpty(list),//在list 不为空的时候,就中断
(list, obj) -> list.addAll(this.findCtry(obj, true))
)
);
原理:
supplier()
: 提供一个初始的空 List。accumulator()
: 负责处理每个元素。关键在于这里,判断中断的条件被加入其中。combiner()
: 在并行流的情况下,合并多个中间结果。finisher()
: 将中间结果转换为最终的 List (这里是直接返回)。characteristics()
: 声明 Collector 的特性 (这里表示finisher
方法不做额外转换)。
代码示例: 见上文。
安全建议: Collector 的代码稍微复杂一些,需要仔细测试。
进阶:
BreakableCollector
可以应用于相似的需求场景。例如,只要稍微调整 toListBreaking
方法中的判断条件即可应用于result
list的Size大于100就中断等等不同的业务场景。这种自定义Collector的技巧具有极强的灵活适应性,推荐有经验的程序员学习与掌握。
总结一下
选择哪种方法取决于你的具体需求、Java 版本以及个人偏好。一般来说:
- 简单情况,能用
for
循环就用。 - 想用 Stream API,就用
anyMatch()
。 - Java 9+ 可以考虑
takeWhile()
。 - 如果遇到比较复杂、有一定通用性的场景,并且想提高代码的可复用性和可读性,可以自定义 Collector。