返回

Java 8 forEach 中断循环构建 List 方案

java

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;//找到了
});

原理:

  1. activeRow 转换成一个 Stream。
  2. 调用 anyMatch() 方法,传入一个 Predicate(判断条件)。
  3. 如果 result 为空,就调用 findCtry 并将结果添加到 result 中,并且由于没有break跳出条件所以返回false, 使anyMatch能继续检查流的元素。
  4. 如果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)));

原理:

  1. takeWhile() 持续获取元素,只要 result.isEmpty() 返回 true
  2. 一旦 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))
        )
);

原理:

  1. supplier(): 提供一个初始的空 List。
  2. accumulator(): 负责处理每个元素。关键在于这里,判断中断的条件被加入其中。
  3. combiner(): 在并行流的情况下,合并多个中间结果。
  4. finisher(): 将中间结果转换为最终的 List (这里是直接返回)。
  5. characteristics(): 声明 Collector 的特性 (这里表示 finisher 方法不做额外转换)。

代码示例: 见上文。

安全建议: Collector 的代码稍微复杂一些,需要仔细测试。

进阶:

BreakableCollector 可以应用于相似的需求场景。例如,只要稍微调整 toListBreaking 方法中的判断条件即可应用于result list的Size大于100就中断等等不同的业务场景。这种自定义Collector的技巧具有极强的灵活适应性,推荐有经验的程序员学习与掌握。

总结一下

选择哪种方法取决于你的具体需求、Java 版本以及个人偏好。一般来说:

  • 简单情况,能用 for 循环就用。
  • 想用 Stream API,就用 anyMatch()
  • Java 9+ 可以考虑 takeWhile()
  • 如果遇到比较复杂、有一定通用性的场景,并且想提高代码的可复用性和可读性,可以自定义 Collector。