用Java 8 Streams替换传统的newForLoop

因此,最后从Java 6跳到Java 8,我已经阅读了相当数量的Java 8 Streams API。 不幸的是,几乎所有被问到的例子几乎都接近于我想要弄清楚怎么做,但还不够接近。

我拥有的是什么

final List<Function> myList = generateList(); final double myVal = calculate(10); private double calculate(double val) { for (Function function : this.myList) { val += function.apply(val); } return val; } 

现在,我已经明白我可以使用.stream().forEach()做类似的事情,但这只适用于foreach和流需要最终变量。 我试图用DoubleStream探索一下得到一个sum() ,但是我需要将当前的和重新应用到每个Function并将该和加到下一个函数中,如上面的代码示例所示。

这可以用纯Stream API吗?

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

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

 static List> myList = Arrays.asList(d -> d + 4, d -> d * 2, d -> d - 3); static Function total=myList.stream() .map(f -> (Function) 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)); } 

产生组合函数的流操作没有相关性问题,理论上甚至可以并行运行(虽然这里没有任何好处),而结果是一个单独的函数,它本身是顺序的,永远不会溶解成需要的部分。是联想的。

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

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

减少需要每个元素并将其聚合(减少)为一个值。 reduce()方法有3种风格 – 这里使用的方法就是诀窍。


一些测试代码:

 static Function a = (d) -> d + 4; static Function b = (d) -> d * 2; static Function c = (d) -> d - 3; static List> 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 

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

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

 static List 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的操作)要求“reduce”操作是左关联的。 这是因为他们希望允许操作并行化,这样一些线程将执行一些操作,然后结果可以组合。 现在,如果你的运算符是乘法而不是除法,那么这不是问题,因为

 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 ,并且您想要组合结果 – 首先无法将正确的值输入到h中。


正如@Bohemian建议的那样,你可能会尝试使用像Stream.reduce这样的方法。 但文档警告你不要这样:

  U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) 

标识值必须是组合器函数的标识。 这意味着对于所有u,组合器(identity,u)等于u。 另外,组合器function必须与累加器function兼容; 对于所有你和你,必须具备以下条件:

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

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

虽然非并行流的当前实现不使用组合器部分,这使得两个条件都不需要 ,但是没有记录 ,并且未来的实现或不同的JRE实现可能决定创建中间结果并使用组合器加入他们,或许是为了提高绩效或任何其他考虑因素。

因此,尽管存在诱惑,但应该使用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操作。 发生这种情况时,它可能看起来更优雅。