Java 8:通过检查所有Stream元素来停止减少操作

时间:2015-06-09 19:06:14

标签: java java-8 reduce java-stream fold

我试图了解是否有办法在不检查整个流的情况下终止还原操作,我无法找到方法。

用例大致如下:让Integer的长列表需要折叠成Accumulator。每个元素检查都可能很昂贵,因此在Accumulator内,我会检查传入的Accumulator,看看我们是否需要执行昂贵的操作 - 如果我们不这样做,那么我只是返回累加器。

对于小型(呃)列表来说,这显然是一个很好的解决方案,但是巨大的列表会产生不必要的流元素访问成本,我希望避免这种情况。

这是代码草图 - 仅假设连续缩减。

class Accumulator {
    private final Set<A> setA = new HashSet<>;
    private final Set<B> setB = new HashSet<>;
}

class ResultSupplier implements Supplier<Result> {

    private final List<Integer> ids;

    @Override
    public Result get() {
        Accumulator acc = ids.stream().reduce(new Accumulator(), f(), (x, y) -> null);

        return (acc.setA.size > 1) ? Result.invalid() : Result.valid(acc.setB);
    }

    private static BiFunction<Accumulator, Integer, Accumulator> f() {
        return (acc, element) -> {
            if (acc.setA.size() <= 1) {
                // perform expensive ops and accumulate results
            }
            return acc;
        };
    }
}

除了必须遍历整个Stream之外,还有一个我不喜欢的事实 - 我必须检查相同的条件两次(即setA大小检查)

我已经考虑了map()collect()的操作,但它们看起来更像是一样,并没有发现它们实质上改变了我无法完成折叠的事实没有检查整个流的操作。

此外,我的想法是假想的takeWhile(p : (A) => boolean) Stream API通讯员也不会给我们买任何东西,因为终止条件取决于累加器,而不是流元素本身。

请记住,我是FP的相对新人,所以有没有办法让这项工作像我期望的那样?我是否设置了不正确的整个问题,还是设计上的限制?

6 个答案:

答案 0 :(得分:8)

而不是以ids.stream()开头,你可以

  1. 使用ids.spliterator()
  2. 将spliterator打包成具有易失性布尔标志的自定义spliterator
  3. 如果标志已更改,则自定义分裂器的tryAdvance返回false
  4. 将您的自定义分割器转换为StreamSupport.stream(Spliterator<T>, boolean)
  5. 的流
  6. 像以前一样继续您的流管道
  7. 通过在累加器已满时切换布尔值来关闭流
  8. 添加一些静态辅助方法以保持其正常运行。

    生成的API可以查看此内容

    Accumulator acc = terminateableStream(ids, (stream, terminator) ->
       stream.reduce(new Accumulator(terminator), f(), (x, y) -> null));
    
      

    另外,我的想法是想象中的takeWhile(p:(A)=&gt; boolean)Stream API通讯员也会买不到我们

    如果条件依赖于累加器状态而不是流成员,它确实有效。这基本上就是我上面概述的方法。

    在JDK提供的takeWhile中可能会被禁止,但使用分裂器的自定义实现可以采用有状态的方法。

答案 1 :(得分:5)

当然,会有一个有趣的,纯粹的FP答案可能有助于以你想要的方式解决这个问题。

与此同时,为什么当简单的解决方案在实际上是必需的并且你的原始数据源是List时,为什么要使用FP,这已经完全实现了,你将使用串行缩减,而不是并行缩减。写下这个:

@Override
public Result get() {
    Accumulator acc = new Accumulator();

    for (Integer id : ids) {
        if (acc.setA.size() <= 1) {
            // perform expensive ops and accumulate results
        }

        // Easy:
        if (enough)
            break;
    }

    return (acc.setA.size > 1) ? Result.invalid() : Result.valid(acc.setB);
}

答案 2 :(得分:3)

正如评论中所述:使用场景听起来有点可疑。一方面,由于reduce而不是collect的使用,另一方面,因为应该用于停止减少的条件这一事实也是出现在累加器中。听起来只是将流限制流到一定数量的元素,或者基于条件,如another question所示,在这里可能更合适。

当然,在实际应用中,可能是条件实际上与已处理的元素数量无关。对于这种情况,我在这里勾勒出一个基本上与answer by the8472对应的解决方案,并且与上述问题的解决方案非常相似:它使用从{{1}创建的Stream只需委托原始Spliterator,除非满足停止条件。

Spliterator
  

编辑以回应评论:

当然,可以把它写成“更实用”。但是,由于问题中的含糊描述和相当“粗略”的代码示例,在这里很难找到“最合适的解决方案”。 (并且“适当”指的是实际任务的具体警告,how functional it should be的问题而不牺牲可读性。)

可能的功能化步骤可能包括创建一个通用import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; public class StopStreamReduction { public static void main(String[] args) { ResultSupplier r = new ResultSupplier(); System.out.println(r.get()); } } class Accumulator { final Set<Integer> set = new HashSet<Integer>(); } class ResultSupplier implements Supplier<String> { private final List<Integer> ids; ResultSupplier() { ids = new ArrayList<Integer>(Collections.nCopies(20, 1)); } public String get() { //return getOriginal(); return getStopping(); } private String getOriginal() { Accumulator acc = ids.stream().reduce(new Accumulator(), f(), (x, y) -> null); return (acc.set.size() > 11) ? "invalid" : String.valueOf(acc.set); } private String getStopping() { Spliterator<Integer> originalSpliterator = ids.spliterator(); Accumulator accumulator = new Accumulator(); Spliterator<Integer> stoppingSpliterator = new Spliterators.AbstractSpliterator<Integer>( originalSpliterator.estimateSize(), 0) { @Override public boolean tryAdvance(Consumer<? super Integer> action) { return accumulator.set.size() > 10 ? false : originalSpliterator.tryAdvance(action); } }; Stream<Integer> stream = StreamSupport.stream(stoppingSpliterator, false); Accumulator acc = stream.reduce(accumulator, f(), (x, y) -> null); return (acc.set.size() > 11) ? "invalid" : String.valueOf(acc.set); } private static int counter = 0; private static BiFunction<Accumulator, Integer, Accumulator> f() { return (acc, element) -> { System.out.print("Step " + counter); if (acc.set.size() <= 10) { System.out.print(" expensive"); acc.set.add(counter); } System.out.println(); counter++; return acc; }; } } 类,该类在委托StoppingSpliterator上运行,并以Spliterator作为停止条件,并以{{1}为其提供在实际的Supplier<Boolean>上,以及在这里和那里使用一些实用方法和方法引用。

但同样:这是否真的是一个合适的解决方案,或者是否应该不使用来自the answer by Lukas Eder的简单实用的解决方案,这是值得商榷的......

Predicate

答案 3 :(得分:3)

没有真正的FP解决方案,因为你的整个累加器不是FP。在这方面我们无法帮助你,因为我们不知道它在做什么。我们所看到的是它依赖于两个可变集合,因此不能成为纯FP解决方案的一部分。

如果您接受这些限制并且使用Stream API没有 clean 方式,那么您可能会尝试使用简单方式。简单的方法包含一个有状态的Predicate,这不是最好的东西,但有时是不可避免的:

public Result get() {
    int limit = 1;
    Set<A> setA=new HashSet<>();
    Set<B> setB=new HashSet<>();
    return ids.stream().anyMatch(i -> {
        // perform expensive ops and accumulate results
        return setA.size() > limit;
    })? Result.invalid(): Result.valid(setB);
}

但我想要注意的是,根据您的具体逻辑,即当集合变得过大时,您的结果被视为无效,您处理不太多元素的尝试是优化错误的案例。你不应该浪费精力去优化它。如果有效结果是处理所有元素的结果,则处理所有元素......

答案 4 :(得分:1)

我认为,可以从自定义收集器(或简化操作)中抛出一个RuntimeException特殊类型,它将结果合并到异常对象中并在collect操作之外捕获它,从而解开结果。我知道使用非异常控制流的例外并不是惯用的,但即使对于并行流,它也适用于你的情况。

实际上,在许多情况下,短路减少可能是有用的。例如,将枚举值收集到EnumSet(一旦发现已经收集了所有可能的枚举值,就可以停止)。或者与Stream<Set>的所有元素相交(如果你的结果集在某个步骤后变为空,你可以停止:继续减少是没用的)。在内部,有一个SHORT_CIRCUIT标记用于流操作,如findFirst,但它没有暴露给公共API。

答案 5 :(得分:1)

我同意以前的所有答案。你通过强制减少可变累加器来做错了。此外,您描述的过程不能表示为转换和缩减的管道。

如果你真的,真的需要做FP风格,我会像@ the8472指出的那样做。

无论如何,我使用迭代器给你一个新的更紧凑的替代方案,类似于@ lukas-eder的解决方案:

Function<Integer, Integer> costlyComputation = Function.identity();

Accumulator acc = new Accumulator();

Iterator<Integer> ids = Arrays.asList(1, 2, 3).iterator();

while (!acc.hasEnough() && ids.hasNext())
  costlyComputation.andThen(acc::add).apply(ids.next());

您对FP有两个不同的顾虑:

如何停止迭代

由于你依赖于可变状态,FP只会让你的生活更加艰难。您可以在外部迭代集合或按照我的建议使用迭代器。

然后,使用if()来停止迭代。

你可以想到不同的策略,但最终,这就是你正在使用的。

我更喜欢迭代器,因为它更具惯用性(在这种情况下表达的意图更好)。

如何设计累加器和昂贵的操作

这对我来说最有趣。

纯函数不能具有状态,必须接收某些东西并且必须返回某些东西,并且对于相同的输入总是相同的东西(如数学函数)。你能表达这样昂贵的操作吗?

它是否需要与累加器共享一些状态?也许共享不属于他们两个。

您是否会转换输入,然后将其附加到累加器中,或者是累加器的责任吗?将函数注入累加器是否有意义?