用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 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> 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
操作,然后您可以将结果相乘并获得与顺序计算中相同的内容。 但对于分裂,这不起作用。
函数应用程序也是非关联的,就像除法一样。 也就是说,如果你有函数f
, g
和h
,并且你运行顺序计算,你最终会得到:
result = val + f(val) + g(val + f(val)) + h(val + f(val) + g(val + f(val)))
现在,如果您有两个中间线程,一个应用f
和g
,另一个应用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操作。 发生这种情况时,它可能看起来更优雅。