【问题标题】:Are Java 8 streams and lambdas deceiving? [closed]Java 8 流和 lambda 是否具有欺骗性? [关闭]
【发布时间】:2014-07-31 14:37:57
【问题描述】:

我已经使用 Java 8 的 lambda 和流有一段时间了,因为我的硕士学位项目和我注意到一些在互联网上没有广泛讨论的东西。我正在使用 Netbeans 进行开发,并且多次建议更改“老式”风格以支持这两个新的构造函数。但我想知道这些建议是否真的有用。要点是:

  • 易读性

也许是习惯问题,但如果您使用嵌套的 lambda,那么了解正在发生的事情可能会成为一场噩梦。

  • 可测试性

由于 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 次比较。

他们在这里:

生成随机数... 开始比较... 开始常规... 总计 = 5.000187629072326E13 完成常规:0.309586459 秒 开始新风格... 总计 = 5.000187629073409E13 完成新样式:20.862798586 秒 开始常规... 总计 = 5.000187629072326E13 完成常规:0.316218488 秒 开始新风格... 总计 = 5.000187629073409E13 完成新样式:20.594838025 秒 [...]

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

作为结论,我可以说您应该谨慎使用这些新结构,由您自行决定,而不是遵循 IDE 建议。

【问题讨论】:

  • 你是在问一个问题并在回答它,还是你实际上在这里问了什么?
  • 我的结论只是一个观点,不一定是被引导到错误假设问题的最佳答案。我希望看到其他开发人员如何面对我指出的问题。
  • 这些功能都不能提高性能。他们怎么可能击败紧密的累积循环。他们在那里是为了代码质量。 (而且减少对代码质量没有帮助。)

标签: lambda java-8 java-stream


【解决方案1】:

尽管有点击率的标题,(“流和 lambdas 是否具有欺骗性?”)我相信这里存在一些实际问题。

如果您说“不要盲目地接受 IDE 建议的重构”,那么请确保这是有道理的。如果生成的代码在某些方面比原始代码更糟糕,则可能是 NetBeans 的重构存在问题。再说一次,IDE 不知道程序员在做什么,并且假设程序员确实知道他或她在做什么,暂时使事情变得更糟的重构不一定是错误。

关于提到的具体点,更具体地分解一下:

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

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

  • 可测试性。 嵌套在某个结构中的任何大块代码都难以测试。这包括长的多行 lambda,出于这个原因和其他原因,我避免使用它。提取方法在这里很有帮助。一种新兴的风格似乎更喜欢由非常简单的 lambda 或方法引用组成的流管道。

  • 可调试性。这可能会令人困惑,并且可能会受到 IDE 调试器与新语言功能的早期问题的阻碍,但我不认为这是一个长期问题.例如,我已经能够使用 NetBeans 8 单步执行多行 lambda。我希望其他 IDE 也能以类似的方式工作。

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

  • 基准测试。使用真正的安全带,例如 JMH,而不是自己动手。顺便说一句,Aleksey Shipilev(JMH 的作者)昨天在JVM Language Summit 上谈到了基准测试,特别是在pitfalls of using nanoTime 上讨论了经过的时间。使用nanoTime 会遇到什么问题会让您大吃一惊。

最后,我不得不说,这是一个非常糟糕的例子。它确实使并行流和 lambda 的性能看起来很糟糕,但dkatzel (+1) 已经在那个方面有所作为。总的来说,代码有很多问题。将随机值添加到 Collection&lt;Number&gt; 中,然后提取 double 值?这比实际计算更像是装箱/拆箱的度量。首先很难就代码得出明智的结论,但如果有问题的代码一开始就不好,那么结论就没有可信度。虽然对数字求和是一个值得怀疑的基准,但合理的方法是从大量double 原语开始,然后比较常规代码和基于流的代码的代码和性能。但是,这将不得不等待另一个时间。

【讨论】:

    【解决方案2】:

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

    你想要这个:

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

    这将使您将 Stream&lt;Double&gt; 变为 DoubleStream(有点像 Stream&lt;double&gt; ),然后使用新的 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 会对性能产生影响。想想运行多线程迭代和求和所需的所有后台工作。

    【讨论】:

    • 总和超过DoubleStream,而不是Stream&lt;double&gt;,否则是的。
    • @StuartMarks 哦,对了,谢谢,我已经更新了答案。
    • 其实是Netbeans建议的。但无论如何,这是一个好点。
    • 我会使用.mapToDouble(Number::doubleValue)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多