在同一 Java 流中聚合值并转换为单一类型

Aggregate values and convert into single type within the same Java stream

我有一个包含 Seed 个元素的集合的 class。 Seed 方法的 return 类型之一是 Optional<Pair<Boolean, String>>.

我正在尝试遍历所有 seeds,查找是否有任何 boolean 值是 true,同时创建一个包含所有 String 值。例如,我的输入格式为 Optional<Pair<Boolean, String>>,输出应为 Optional<Signal>,其中 Signal 类似于:

class Signal {
   public boolean exposure;

   public Set<String> alarms;

   // constructor and getters (can add anything to this class, it's just a bag)
}

这是我目前可用的:

// Seed::hadExposure yields Optional<Pair<Boolean, String>> where Pair have key/value or left/right
public Optional<Signal> withExposure() {
  if (seeds.stream().map(Seed::hadExposure).flatMap(Optional::stream).findAny().isEmpty()) {
    return Optional.empty();
  }
  final var exposure = seeds.stream()
      .map(Seed::hadExposure)
      .flatMap(Optional::stream)
      .anyMatch(Pair::getLeft);
  final var alarms = seeds.stream()
      .map(Seed::hadExposure)
      .flatMap(Optional::stream)
      .map(Pair::getRight)
      .filter(Objects::nonNull)
      .collect(Collectors.toSet());
  return Optional.of(new Signal(exposure, alarms));
}

现在我有时间让它变得更好,因为 Seed::hadExposure 可能会变得很昂贵,所以我想看看我是否可以只通过一次就完成所有这些。我已经尝试过 reduce、使用收集器(Collectors.collectingAndThenCollectors.partitioningBy 等)(来自之前问题的一些建议),但到目前为止还没有。

可以在单个 stream() 表达式中执行此操作,使用 map 将 non-empty 曝光转换为 Signal,然后将 reduce 转换为合并信号:

Signal signal = exposures.stream()
    .map(exposure ->
        new Signal(
            exposure.getLeft(),
            exposure.getRight() == null
                ? Collections.emptySet()
                : Collections.singleton(exposure.getRight())))
    .reduce(
        new Signal(false, new HashSet<>()),
        (leftSig, rightSig) -> {
            HashSet<String> alarms = new HashSet<>();
            alarms.addAll(leftSig.alarms);
            alarms.addAll(rightSig.alarms);
            return new Signal(
                leftSig.exposure || rightSig.exposure, alarms);
        });

但是,如果您有很多警报,成本会很高,因为它会创建一个新的 Set 并将新警报添加到输入中每次曝光的累积警报中。

在从 ground-up 设计用于支持函数式编程的语言中,例如 Scala 或 Haskell,您将拥有一个 Set 数据类型,可以让您 efficiently create a new set that's identical to an existing set 但是添加了一个元素,所以没有效率上的担忧:

filteredSeeds.foldLeft((false, Set[String]())) { (result, exposure) => 
  (result._1 || exposure.getLeft, result._2 + exposure.getRight)
}

但是 Java 没有开箱即用的东西。

您可以只为结果创建一个 Set 并在流的 reduce 表达式中改变它,但有些人会认为这是糟糕的风格,因为您会混合使用函数范式( map/reduce 在流上)与程序流(改变一组)。

就我个人而言,在 Java 中,我只是放弃函数式方法,在这种情况下使用 for 循环。它将更少的代码、更高的效率和更清晰的 IMO。

如果您有足够的 space 来存储中间结果,您可以这样做:

List<Pair<Boolean, String>> exposures = 
    seeds.stream()
        .map(Seed::hadExposure)
        .flatMap(Optional::stream)
        .collect(Collectors.toList());

那么您只需为输入列表中的每个项目调用一次昂贵的 Seed::hadExposure 方法。