有许许多多关于 Java 8 中流效率的讨论,但根据 Alex Zhitnitsky 的测试结果显示:坚持使用传统的 Java 编程风格——iterator 和 for-each 循环——比 Java 8 的实现性能更佳。


Java 8 中的 Lambda 表达式和流(Stream)受到了热烈欢迎。这是 Java 迄今为止最令人激动的特征。这些新的语言特征允许采用函数式风格来进行编码,我们可以用这些特性完成许多有趣的功能。这些特性如此有趣以至于被认为是不合理的。我们对此表示怀疑,于是决定对这些特性进行测试。


我们创建一个简单的任务:从一个 ArrayList 找出最大值,将传统方式与 Java 8 中的新方式进行测试比较。说实话,测试的结果让我感到非常惊讶。


命令式风格与 Java 8 函数式编程风格比较


我喜欢直接进入主题,所以先看一下结果。为了做这次基准测试,我们先创建了一个 ArrayList,并插入一个 100000 个随机整数,并通过 7 种不同的方式遍历所有的值来查找最大值。实现分为两组:Java 8 中引入的函数式风格与 Java 一直使用的命令式风格。


这是每个方法耗费的时长:


图片


最大错误记录是并行流上的 0.042,完整输出结果在这篇文章结尾部分可以看到。


小贴士:


哇哦!Java 8 中提供的任何一种新方式都会产生约 5 倍的性能差异。有时使用简单迭代器循环比混合 lambda 表达式和流更有效,即便这样需要多写几行代码,且需要跳过甜蜜的语法糖(syntactic suger)。


使用迭代器或 for-each 循环是遍历 ArrayList 最有效的方式,性能比采用索引值的传统 for 循环方式好两倍。


在 Java 8 的方法中,并行流的性能最佳。但是请小心,在某些情况下它也可能会导致程序运行得更慢。


Lambda 表达式的速度介于流与并行流之间。这个结果确实挺令人惊讶的,因为 lambda 表达式的实现方式是基于流的 API 来实现的。


不是所有的情况都如上所示:当我们想演示在 lambda 表达式和流中很容易犯错时,我们收到了很多社区的反馈,要求我们优化基准测试代码,如消除整数的自动装包和解包操作。第二次测试(已优化)的结果在这篇文章结束位置可以看到。


让我们快速看一下每个方法,按照运行速度由快到慢:


命令式风格


iteratorMaxInteger()——使用迭代器遍历列表:


public int iteratorMaxInteger() {

    int max = Integer.MIN_VALUE;

    for (Iterator it = integers.iterator(); it.hasNext(); ) {

        max = Integer.max(max, it.next());

    }

    return max;

}


forEachLoopMaxInteger()——不使用迭代器,使用 For-Each 循环遍历列表(不要误用 Java 8 的 forEach)


public int forEachLoopMaxInteger() {

    int max = Integer.MIN_VALUE;

    for (Integer n : integers) {

        max = Integer.max(max, n);

    }

    return max;

}


forMaxInteger()——使用简单的 for 循环和索引遍历列表:


public int forMaxInteger() {

    int max = Integer.MIN_VALUE;

    for (int i = 0; i < size; i++) {

        max = Integer.max(max, integers.get(i));

    }

    return max;

}


函数式风格


parallelStreamMaxInteger()——使用 Java 8 并行流遍历列表:


public int parallelStreamMaxInteger() {

    Optional max = integers.parallelStream().reduce(Integer::max);

    return max.get();

}


lambdaMaxInteger()——使用 lambda 表达式及流遍历列表。优雅的一行代码:


public int lambdaMaxInteger() {

    return integers.stream().reduce(Integer.MIN_VALUE, (a, b) -> Integer.max(a, b));

}


forEachLambdaMaxInteger()——这个用例有点混乱。可能是因为 Java 8 的 forEach 特性有一个很烦人的东西:只能使用 final 变量,所以我们创建一个 final 包装类来解决该问题,这样我们就能访问到更新后的最大值。


public int forEachLambdaMaxInteger() {

    final Wrapper wrapper = new Wrapper();

    wrapper.inner = Integer.MIN_VALUE;

 

    integers.forEach(i -> helper(i, wrapper));

    return wrapper.inner.intValue();

}

 

public static class Wrapper {

    public Integer inner;

}

 

private int helper(int i, Wrapper wrapper) {

    wrapper.inner = Math.max(i, wrapper.inner);

    return wrapper.inner;

}


顺便提一下,如果要讨论 forEach,我们提供了一些有趣的关于它的缺点的见解,答案参见 StackOverflow。


streamMaxInteger()——使用 Java 8 的流遍历列表:


public int streamMaxInteger() {

    Optional max = integers.stream().reduce(Integer::max);

    return max.get();

}


优化后的基准测试


根据这篇文章的反馈,我们创建另一个版本的基准测试。源代码的不同之处可以在这里查看。下面是测试结果:



图片


修改总结:


  • 列表不再用 volatile 修饰。


  • 新方法 forMax2 删除对成员变量的访问。


  • 删除 forEachLambda 中的冗余 helper 函数。现在 lambda 表达式作为一个值赋给变量。可读性有所降低,但是速度更快。


  • 消除自动装箱。如果你在 Eclipse 中打开项目的自动装箱警告,旧的代码会有 15 处警告。


  • 优化流代码,在 reduce 前先使用 mapToInt。


  • 非常感谢 Patrick Reinhart, Richard Warburton, Yan Bonnel, Sergey Kuksenko, Jeff Maxwell, Henrik Gustafsson 以及每个 Twitter 上评论的人,感谢你们的贡献。


测试基础


我们使用 JMH(Java Microbenchmarking Harness) 执行基准测试。如果想知道怎么将其应用在你自己的项目中,可以参考这篇文章,我们通过一个自己写的实例来演示 JMH 的主要特性。


基础测试的配置包含 2 个JVM、5 次预热迭代和 5 次测量迭代。该测试运行在 c3.xlarge Amazon EC2 实例上(CPU:4 核,内存:7.5G,存储:2 x 40 GB SSD),采用 Java 8u66 和 JMH 1.11.2。所有的源代码都在 GitHub 上,你可以在这里看到原始的输出结果。


顺便做一下免责申明:基准测试往往不是完全可信的,也很难保证绝对正确。虽然我们试图以最准确的方式来运行,但仍然建议接受结果时抱有怀疑的态度。


最后的思考


开始使用 Java 8 的第一件事情是在实践中使用 lambda 表达式和流。但是请记住:它确实非常好,好到可能会让你上瘾!但是,我们也看到了,使用传统迭代器和 for-each 循环的 Java 编程风格比 Java 8 中的新方式性能高很多。


当然,这也不是绝对的。但这确实是一个相当常见的例子,它显示可能会有大约 5 倍的性能差距。如果这影响到系统的核心功能或成为系统一个新的瓶颈,那就相当可怕了。


更多相关文章

  1. 测试领域,小白问题大集合(适合未入门和入门初级者)
  2. Swagger 自动生成 Dubbo 服务的接口文档,以及测试调用
  3. Linux性能优化(八)——网络测试工具
  4. 用 cURL 请求测试 ETag 浏览器缓存[每日前端夜话0xCC]
  5. Linux性能优化(十三)——CPU性能测试
  6. jQuery遍历祖先元素:parentsUntil
  7. jQuery在点击按钮上迭代/循环遍历数据表
  8. 如何在angularjs代码中单元测试jquery元素
  9. jQuery遍历----------(遍历、祖先、后代、同胞、过滤)

随机推荐

  1. Android异步双屏显示
  2. 【android】【Bootanimation】Bootanimat
  3. android基础画板的实现(surfaceView)
  4. 使用android的加速度计移动图像
  5. android.os.NetworkOnMainThreadExceptio
  6. 将JSON数据传递到SQLite并检索它
  7. Android的startActivityForResult()与onA
  8. C# PC客户端与Android服务端的Socket同步
  9. Android中使用HorizontalScrollView横向
  10. Android使用 startActivityForResult 、