【问题标题】:Replacing traditional newForLoop with Java 8 Streams用 Java 8 Streams 替换传统的 newForLoop
【发布时间】:2015-09-01 18:27:21
【问题描述】:

所以,我终于从 Java 6 到 Java 8 有了较大的飞跃,我已经阅读了相当多的 Java 8 Streams API。不幸的是,几乎所有被问到的例子都几乎接近我试图弄清楚如何做的事情,但还不够接近。

我拥有的是

final List<Function<? super Double, Double>> myList = generateList();
final double myVal = calculate(10);

private double calculate(double val) {
    for (Function<? super Double, Double> function : this.myList) {
        val += function.apply(val);
    }
    return val;
}

现在,我开始明白我可以用 .stream().forEach() 做类似的事情,但这仅适用于 foreach 并且流需要最终变量。我尝试使用DoubleStream 进行一些探索以获得sum(),但我需要将当前总和重新应用于每个Function 并将该总和添加到下一个函数,如上面的代码示例显示。

纯 Stream API 可以做到这一点吗?

编辑:所以在使用reduce() 区域进行测试后,我对执行此类计算所需的时间进行了一个简单的测试,结果不利于流。这是一个例子https://gist.github.com/gabizou/33f616c08bde5ab97e56。包括来自相当基本测试的日志输出。

【问题讨论】:

  • 不,不是。 Stream 操作实际上不适用于像您正在做的那样的固有顺序操作。
  • 我明白,我只是希望流最终具有相同的性能。在宏伟的计划中,很可能对于大量功能,这可能会以平价告终,但对于我正在做的事情,这不太可能。当然,随着我们开发新的 Java 版本,我们可能会看到优化和增强。

标签: java foreach lambda java-8 java-stream


【解决方案1】:

您可以使用流 API 从您的函数列表中组合一个函数。

static List<Function<? super Double, Double>> myList
    = Arrays.asList(d -> d + 4, d -> d * 2, d -> d - 3);

static Function<Double, Double> total=myList.stream()
    .map(f -> (Function<Double, Double>) d -> d + f.apply(d))
    .reduce(Function::andThen).orElse(Function.identity());

static double calculate(double val) {
    return total.apply(val);
}

public static void main(String[] args) {
    System.out.println(calculate(10));
}

产生组合函数的流操作不存在关联性问题,理论上甚至可以并行运行(尽管这里没有任何好处),而结果是单个函数,它本身是连续的,从不分解为部分这需要是关联的。

【讨论】:

  • 请注意:并行化这不仅仅是“没有好处”,而是可怕。在顺序模式下,它甚至可以很好地运行 1000 个函数。使用并行流......即使有 10 个函数,它似乎也会陷入僵局。
  • @RealSkeptic:死锁似乎是由在类的 static 初始化程序中运行引起的。当我将代码移动到一个方法中时,运行没有问题。我会调查这个问题,因为它似乎是一个严重的错误。
  • @RealSkeptic:问题在于 lambda 表达式会生成新类,这些类会在其定义类上调用合成方法。在未完全初始化的类上执行的异步方法调用将等待初始化完成,因此,如果初始化程序本身等待该异步操作的完成,您就会遇到死锁。因此,static 初始化程序中的并行流操作是行不通的。
  • 感谢您的解释。这可能应该在某处用粗体写。也许您应该创建一个带有自我回答的问题。
  • @Leonardo Pina:组合不像f(x)+g(x)+h(x),它像h(g(f(x))),即一个函数的输出是下一个函数的输入。在这种特殊情况下,每个函数 f 都投影到 x+f(x),因此,生成的函数类似于 h(g(f(x)+x)+f(x)+x)+g(f(x)+x)+f(x)+x。这也是问题循环正在做的事情;它将函数的结果添加到val,但val 是下一个函数的参数。
【解决方案2】:

是的,您可以通过执行缩减来使用流解决方案:

private double calculate(double val) {
    return myList.stream().reduce(val, (d, f) -> d + f.apply(d), (a, b) -> a + b);
}

归约获取每个元素并将其聚合(归约)为一个值。 reduce() 方法有 3 种风格 - 这里使用的一种可以解决问题。


一些测试代码:

static Function<? super Double, Double> a = (d) -> d + 4;
static Function<? super Double, Double> b = (d) -> d * 2;
static Function<? super Double, Double> c = (d) -> d - 3;
static List<Function<? super Double, Double>> myList = Arrays.asList(a, b, c);

static double calculate(double val) {
    return myList.stream().reduce(val, (d, f) -> d + f.apply(d), (a, b) -> a + b);
}

public static void main(String[] args) {
    System.out.println(calculate(10));
}

输出:

141.0

【讨论】:

  • 我不认为它做同样的事情。原来的,对于三个函数 f,g,h 会给你 f(val) + g(f(val)) + h(f(val) + g(f(val))),而 reducer 允许组合部分结果使用+,因此您最终可能会得到 f(val) + g(val) + h(val) 或类似的东西。这里的组合器不符合文档中的约束。
  • 我只是在逻辑上进行了测试,这确实是代码的作用。 10 + (10 + 4) = 24 THEN 24 + (24 *2) = 72 THEN 72 + (72 - 3) = 141 最终结果还是一样的。现在我真的很好奇在这种情况下使用流与传统方法相比的性能成本。
  • 实际上这是(希望)即将到来的foldLeft 的工作。这里的操作是非关联的,所以 reduce 只会并行产生不正确的结果。
  • 就是这样,不是吗?组合器从未被调用的事实是未记录的,并且是一个实现细节,应该被依赖,这是非常有目的的,任何不会产生应避免同一流的并行和非并行版本的结果相同。
  • 如果代码依赖于从未调用过的组合器,它应该做的最低限度是提供(x,y)-&gt;{throw new AssertionError();} 作为组合器,而不是默默地产生错误值的东西。 @gabizou:您的性能测试缺少任何预热阶段,因此您正在测量流框架的类加载和初始化时间。
【解决方案3】:

这个特定的例子对于 Java 8 流来说是非常有问题的。它们专为顺序不重要的操作而设计。

函数应用程序不是关联的。为了解释,让我们举一个更简单的例子,其中一个人想要取一个数字并将其除以一个数字列表:

static List<Double> dividers = Arrays.asList( 3.5, 7.0, 0.5, 19.0 );

public double divideByList( double a ) {
    for ( Double d : dividers ) {
        a /= d;
    }
    return a;
}

所以,你得到的是

a ÷ 3.5 ÷ 7.0 ÷ 0.5 ÷ 19.0

算术很简单 - 除法是左结合的,这意味着这等价于

a ÷ ( 3.5 × 7.0 × 0.5 × 19.0)

不是

a ÷ ( 3.5 ÷ 7.0 ÷ 0.5 ÷ 19.0 )

不是

( a ÷ 3.5 ÷ 7.0 ) ÷ ( 0.5 ÷ 19.0 )

基于reduce/collectors 的流操作要求“reducing”操作是左关联的。这是因为他们想让操作被并行化,这样一些线程会做一些操作,然后可以合并结果。现在,如果您的 our 运算符是乘法而不是除法,这将不是问题,因为

a × 3.5 × 7.0 × 0.5 × 19.0

相同
(a × 3.5 × 7.0 ) × (0.5 × 19)

这意味着一个线程可以执行a × 3.5 × 7.0,另一个可以执行0.5 × 19.0 操作,然后您可以将结果相乘并得到与顺序计算相同的结果。但是对于除法,这是行不通的。

函数应用也是非关联的,就像除法一样。也就是说,如果您有函数fgh,并运行顺序计算,您将得到:

result = val + f(val) + g(val + f(val)) + h(val + f(val) + g(val + f(val)))

现在,如果您有两个中间线程,一个应用fg,另一个应用h,并且您想要组合结果 - 没有办法将正确的值放入@首先是987654340@。


您可能很想尝试使用Stream.reduce 之类的方法,正如@Bohemian 所建议的那样。但是documentation 警告您不要这样做:

<U> U reduce(U identity,
             BiFunction<U,? super T,U> accumulator,
             BinaryOperator<U> combiner)

...

标识值必须是组合器函数的标识。这意味着对于所有 u,combiner(identity, u) 等于 u。此外,combiner 函数必须与 accumulator 函数兼容;对于所有 u 和 t,必须满足以下条件:

combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)

对于像+ 这样的操作,标识为0。对于*,标识为1。因此,将val 用作identity 是违反文档的。而第二个条件就更成问题了。

虽然非并行流的当前实现不使用 combiner 部分,这使得这两个条件都不需要,这没有记录 strong>,未来的实现或不同的 JRE 实现可能会决定创建中间结果并使用 combiner 来加入它们,可能是为了提高性能或出于任何其他考虑。

因此,尽管有诱惑,但应该使用Stream.reduce 来尝试模仿最初的顺序处理。


一种方法可以做到这一点,实际上并不会破坏文档。它涉及保留一个保存结果的可变对象(它必须是一个对象,以便它在仍然可变的同时有效地最终确定),并使用Stream.forEachOrdered,它保证操作将按照它们在流中出现的顺序执行, 如果流是有序的。并且列表的流具有定义的顺序。即使您使用myList.stream().parallel(),这也有效。

public static double streamedCalculate(double val) {
    class MutableDouble {
        double currVal;
        MutableDouble(double initVal) {
            currVal = initVal;
        }
    }

    final MutableDouble accumulator = new MutableDouble(val);

    myList.stream().forEachOrdered((x) -> accumulator.currVal += x.apply(accumulator.currVal));
    return accumulator.currVal;
}

就个人而言,我发现您的原始循环比这更具可读性,因此在这里使用流确实没有任何优势。

根据@Tagir Valeev 的评论,计划在未来的Java 版本中使用foldLeft 操作。发生这种情况时,它可能看起来更优雅。

【讨论】:

    猜你喜欢
    • 2019-11-24
    • 2016-11-16
    • 2015-06-19
    • 2022-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-12
    相关资源
    最近更新 更多