Java 8流和lambdas是否具有欺骗性?

时间:2014-07-31 14:37:58

标签: lambda java-8 java-stream

我已经使用Java 8的lambdas和流了一段时间,因为我的硕士学位项目,我注意到一些在互联网上没有广泛讨论过的东西。我使用Netbeans进行开发,很多时候它建议改变老式的#34;风格有利于这两个新的构造函数。但我想知道这些建议是否真的有用。要点是:

  • 易读性

也许是习惯问题,但如果你使用嵌套的lambdas,那么理解正在发生的事情就变成了一场噩梦。

  • 可测试性

由于Netbeans的建议,我们倾向于将for循环更改为流的foreach调用,但是在可测试性方面存在微妙但非常危险的副作用。如果你的代码在foreach块中失败,那么IDE(实际上是编译器)根本不知道错误发生在哪一行,指向块的开始。此外,调试代码更加困难,因为我们无法控制计算和内部循环。

  • 性能

同样,IDE总是建议将累积更改为一种map reduce算法。后者看起来更复杂,所以我创建了一个简单的测试来检查这种方法有多好。令人惊讶的是,慢了

代码:

public class Java8Kata {

public static void main(String[] args) {

    System.out.println("Generating random numbers...");
    final Collection<Number> numbers = getRandomNumbers();

    System.out.println("Starting comparison...");

    for (int i = 0; i < 20; i++) {
        getTotalConventionalStyle(numbers);
        getTotalNewStyle(numbers);
    }
}

public static void getTotalConventionalStyle(Collection<Number> numbers) {

    long startTime = System.nanoTime();
    System.out.println("\n\nstarting conventional...");
    double total = 0;
    for (Number number : numbers) {
        total += number.doubleValue();
    }
    System.out.println("total = " + total);

    System.out.println("finish conventional:" + getPeriod(startTime) + " seconds");
}

public static void getTotalNewStyle(Collection<Number> numbers) {

    long startTime = System.nanoTime();
    System.out.println("\n\nstarting new style ...");

    double total = 0;
    //netbeans conversion
    total = numbers.parallelStream().map((number) -> number.doubleValue()).reduce(total, (accumulator, _item) -> accumulator + _item);
    System.out.println("total = " + total);

    System.out.println("finish new style:" + getPeriod(startTime) + " seconds");
}

public static Collection<Number> getRandomNumbers() {

    Collection<Number> numbers = new ArrayList<>();

    for (long i = 0; i < 9999999; i++) {
        double randomInt = 9999999.0 * Math.random();
        numbers.add(randomInt);
    }
    return numbers;
}

public static String getPeriod(long startTime) {
    long time = System.nanoTime() - startTime;
    final double seconds = ((double) time / 1000000000);
    return new DecimalFormat("#.##########").format(seconds);
}

}

为了确保结果一致,我进行了20次比较。

他们是:

Generating random numbers...
Starting comparison...


starting conventional...
total = 5.000187629072326E13
finish conventional:0.309586459 seconds


starting new style ...
total = 5.000187629073409E13
finish new style:20.862798586 seconds


starting conventional...
total = 5.000187629072326E13
finish conventional:0.316218488 seconds


starting new style ...
total = 5.000187629073409E13
finish new style:20.594838025 seconds

[...]

我的目标不是进行深度的性能测试,我只想看看Netbeans是否在帮助我。

作为结论,我可以说你应该根据自己的决定仔细使用这些新结构,而不是遵循IDE的建议。

2 个答案:

答案 0 :(得分:5)

尽管有点击率标题,(&#34;是溪流和lambdas欺骗?&#34;)我相信这里有一些真正的问题。

如果你说'不要盲目接受IDE提出的重构&#34;那肯定是有道理的。可能是NetBeans存在问题&#39;如果结果代码在某些方面比原始代码更糟,则重构。再说一遍,IDE并不知道程序员正在做什么,并假设程序员 知道他或她在做什么,暂时让事情变得更糟的重构不是必然的一个错误。

在提到的具体问题上,更具体地分解了一下:

  • 易读性。是的,lambdas和溪流会让事情变得更糟。但他们也可以做得更好,更好。可以使用任何语言和库构造编写错误的代码。

  • 编译时错误。这些错误,尤其是与类型推断相关的错误,可能会造成混淆。如果我在编写长管道时遇到问题,通常我会将表达式分解为临时表达。

  • 可测试性。在某些结构中嵌套的任何大量代码都很难测试。这包括长的多行lambda,我为此避免了这种情况。提取方法在这里非常有用。一种新兴的风格似乎更倾向于由非常简单的lambda或方法引用组成的流管道。

  • 可调试性。这可能令人困惑,并且可能受到IDE早期问题的阻碍。调试器与新语言功能,但我不认为这是一个长期问题。例如,我已经能够使用NetBeans 8单步执行多行lambdas。我希望其他IDE可以比较有效。

  • 表现。程序员始终需要知道自己在做什么,并且开发绩效的心理模型是必要的。 Lambda,流和并行性是Java 8中的新功能(在撰写本文时只有几个月)需要一些时间。两个快速点:1)建立并行管道的成本很重要,并且必须在流元素的处理上分摊。 2)处理原语是有点麻烦,但你必须注意,以免自动装箱和自动拆箱杀死你的表现。这显然是在这里。

  • 基准测试。使用JMH之类的真实线束,而不是滚动自己的线束。顺便提一下,Aleksey Shipilev(JMH作者)昨天在JVM Language Summit基准测试时发表了讲话,特别是在pitfalls of using nanoTime上测量了经过的时间。您会对使用nanoTime可能遇到的问题感到震惊。

最后,我不得不说,这是一个非常糟糕的例子。它肯定会使并行流和lambda的性能看起来很糟糕,但是dkatzel(+1)在那个时候已经开始了。总体而言,代码存在大量问题。将随机值添加到Collection<Number>,然后提取double值?这更像是对装箱/拆箱的衡量,而不是真正的计算。一开始就很难得出关于代码的合理结论,但如果所讨论的代码开头不好,那么结论就没有可信度。虽然求和数是一个可疑的基准,但合理的方法是从大量double基元开始,并比较传统和基于流的代码的代码和性能。然而,那将不得不等待另一次。

答案 1 :(得分:4)

你没有做正确的新风格总结

你想要这个:

total = numbers.parallelStream()
                .mapToDouble(number -> number.doubleValue())
                .sum();

这会让你Stream<Double>变为DoubleStream(有点像Stream<double>),然后使用新的sum()缩小,这是一个原始求和,不是对象求和,计算时间要快得多。

这也更容易阅读。

当我通过这个简单的代码更改在我的机器上运行时,我得到了这个:

Generating random numbers...
Starting comparison...

finish conventional:  0.078106 seconds
finish new style:     0.279964 seconds


finish conventional: 0.126721 seconds
finish new style:    0.045977 seconds

 .... etc

这比您的方法快100倍,基本上和传统方法一样快。运行新流API会对性能产生影响。考虑运行多线程迭代和求和所需的所有后台工作。