【问题标题】:Is it possible to iterate a stream only once and perform 2 or more operations?是否可以只迭代一次流并执行 2 次或更多操作?
【发布时间】:2015-10-07 11:16:25
【问题描述】:

给定代码

List<Integer> numbers = Arrays.asList(2, 4, 3);

int sumTotal = numbers.stream().reduce(-3, (x, y) -> x + y + 3);
int multiplyTotal = numbers.stream().reduce(1, (x, y) -> x * y);

是否可以在只迭代一次流的同时执行这两个操作?

另外,请注意每个 reduce 都有不同的标识:-3 和 1。

【问题讨论】:

标签: functional-programming java-8 reduce


【解决方案1】:

您可以使用我在this answer 中写的配对收集器:

int[] result = numbers.stream().collect(
        pairing(
                Collectors.reducing(0, (x, y) -> x + y + 3),
                Collectors.reducing(1, (x, y) -> x * y),
                (sum, prod) -> new int[] { sum, prod }));

当然,您可以以任何其他方式组合结果。将它们放入数组只是一个示例。

这个收集器在我的 StreamEx 库 (MoreCollectors.pairing) 中很容易找到。或者,您可以使用 jOOL 库:它具有 Tuple.collectors 方法,其工作方式类似。

【讨论】:

  • 很好,你的库是否也支持超过 2 个收集器?
  • 不,最多两个。好吧,您可以使用pairing(pairing(col1, col2, combine1_2), col3, combine1_2_3) 链接这个pairing 方法,但这很奇怪。我决定将两个收集器组合起来不太常见和有用,但是组合三个或更多收集器太多了,代码会变得非常混乱,所以最好像@JBNizet 建议的那样定义新对象。此外,我决定(至少现在)不在我的 lib 中定义新的功能接口和用户可见的数据容器,如元组,并且 JDK 中没有 TriFunction。 AFAIC Jool 有现成的方法可以组合多达 8 个收集器,因此您可以使用它。
【解决方案2】:

您可以创建自定义类,并使用可变归约:

public static void main(String[] args) {
    List<Integer> numbers = Arrays.asList(2, 4, 3);

    Result result = numbers.stream().collect(Result::new, Result::consume, Result::combine);
    System.out.println("result = " + result);
}

private static class Result {
    private int sum = 0;
    private int product = 1;

    public void consume(int i) {
        sum += i + 3;
        product *= i;
    }

    public void combine(Result r) {
        // READ note below
        sum += r.sum + 3;
        product *= r.product;
    }

    @Override
    public String toString() {
        return "Result{" +
            "sum=" + sum +
            ", product=" + product +
            '}';
    }
}

但我怀疑这样做会比简单地迭代两次获得更多收益。

编辑:

请注意,使用并行流时调用的上述 combine() 方法将产生与顺序流不一致的结果。正如 Holger 在 cmets 中提到的那样,问题是由于该操作不尊重归约的身份先决条件:对任何具有身份的值 V 应用归约应该产生 V,但它会产生 V + 3 以 0 为标识。

确保不要使用并行流来计算该结果。

【讨论】:

  • 你说得对。您并没有让它变得更快,并且生成的代码更难理解。 @Tagir 的解决方案稍微好一些。在性能/可读性方面,我的更接近原始......你只需要创建一个一次性类。或者,在 Java 8 中,您可以在 Map 中使用整数吗?
  • 你的解决方案其实和我的一样。主要区别在于,由于操作应该是可并行的,因此我必须提供一个您的 JS 代码不必提供的组合操作。使用地图会使情况变得更糟:可读性降低,意味着更多的装箱/拆箱操作,以及地图上的 get()/put()。
  • 您的组合器错误。为了正确归约,组合器必须对部分结果应用与顺序归约相同的函数,即x+y+3,而不仅仅是求和。有趣的是,它似乎产生了一致的结果,因为它补偿了提问者的错误:0 不是x+y+3 的标识值。所以每个线程都被3 关闭,你的组合器通过不添加3 来补偿,因此最终结果总是相同的,被3 关闭,就像问题的原始代码一样。但是正确的结果应该和.reduce((x, y) -&gt; x + y + 3).get()的结果一样
  • 我不认为combiner的任务是验证函数约束的正确性。我也不明白它是怎么做到的。请注意,如果您未能提供正确的身份,流的标准 reduce 实现不会对其进行验证,但也会产生不一致的结果。
  • 好吧,使用-3 作为标识值也可以解决它……
【解决方案3】:

在 JS 中,一种解决方案是遍历对象而不是数字。例如:

  numbers.stream().reduce({sum:0,prod:1}, (memo, cur) ->
       memo.sum = memo.sum + cur + 3;
       memo.prod = memo.prod * cur;
       return memo;
  )

请注意,在您的示例中,x 是“备忘录”(或累加器),y 是当前值。

同样的想法也应该适用于 Java。

【讨论】:

    猜你喜欢
    • 2016-09-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-05
    • 1970-01-01
    • 2011-03-30
    • 1970-01-01
    • 2017-12-20
    • 1970-01-01
    相关资源
    最近更新 更多