用Java 8 Streams替换传统的newForLoop

时间:2015-09-01 18:27:22

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

所以,最后从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和stream需要最终变量。我尝试使用DoubleStream进行探索以得到sum(),但我需要将当前总和重新应用于每个Function并将该总和添加到下一个函数,如上面的代码示例所示。

这是否可以使用纯Stream API?

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

3 个答案:

答案 0 :(得分:10)

您可以使用流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));
}

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

答案 1 :(得分:3)

是的,您可以通过执行缩减

来使用流解决方案
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

答案 2 :(得分:0)

这个特殊的例子对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的操作,需要&#34;减少&#34;操作将是左关联的。这是因为他们希望允许操作并行化,这样一些线程将执行一些操作,然后结果可以组合。现在,如果你的运算符是乘法而不是除法,那么这不是问题,因为

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这样的方法。但documentation警告你不要这样做:

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

...

     

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

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

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

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

所以,尽管存在诱惑,但使用identity来尝试模仿原始的顺序处理。

一种方法,它实际上并没有破坏文档。它涉及保持一个保存结果的可变对象(它必须是一个对象,以便它仍然是有效的最终,同时仍然是可变的),并使用Stream.forEachOrdered,这保证了操作将在如果订购了流,则它们会出现在流中。并且列表的流具有已定义的顺序。即使您使用Stream.reduce,这也可以。

myList.stream().parallel()

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

基于@Tagir Valeev的评论,计划在未来的Java版本中进行foldLeft操作。当发生这种情况时,它可能看起来更优雅。