趣文网 > 作文大全

lambda表达式速度如何呢?看完这篇文章你就明白了

2020-11-25 09:45:01
相关推荐

虽然Java 8已经出了好几年了,但是很多朋友可能对于其中的一些特性还是不太了解。甚至对lambda表达式这个特性可能会产生误解,误认为lambda表达式会影响程序的速度。其中也不乏很多误人子弟的自媒体传播这些错误的观点。

今天我看到一篇自媒体推送的文章,号称用Java字节码分析为什么lambda表达式速度慢,但是其中漏洞百出,搞得我忍不住写了这么一篇文章,为一些受到误导的朋友纠正一个概念:lambda表达式和普通的循环一样,不会影响到程序速度,大家可以放心使用。

因为头条压缩图片的缘故,所以对于小段代码,我用高亮代码图片的形式贴出。对于大段代码,直接贴代码,可能会影响大家的阅读体验。也希望头条能够允许上传高清图片,让大家的阅读体验更好一下。

lambda表达式是什么

可能还有一些朋友对lambda表达式还是不太清楚,所以我先介绍一下lambda表达式的概念。简单来说lambda表达式就是匿名函数,在一些支持匿名函数的语言中,用不用lambda表达式其实不是那么重要。但是因为Java不支持匿名函数,所以lambda表达式可以极大的简化这些场合的代码。

先来看看一个例子。假如我们需要在一个新线程中运行代码,可能需要创建一个新的Runnable对象。此处使用了Java的一项特性匿名内部类,创建了一个新的临时的Runnable对象。但是代码如你所见非常难看,大段的缩进和方括号,非常影响阅读。

如果换成了lambda表达式的实现,如你所见,代码非常干净整洁。

这种形如(a,b)->{ ..... }的表达式就是lambda表达式。上面已经提到过了,lambda表达式其实就是匿名函数,箭头前面的括号内部的就是函数的参数列表;箭头后面的括号内部的就是方法体,假如方法体只有一行语句或者表达式,方法体的括号可以省略。

lambda表达式参数的类型也不需要写明,编译器会自动从前面的类型中推断。在上面的例子中,因为Runnable中的run函数没有参数,所以lambda表达式自然也不需要参数。你可能会想到,假如类型中有多个函数怎么办呢?这时候编译器无法推断,程序就会报错。这也是Java lambda表达式的一个限制,前面的接口类型中只能有一个函数声明。

很多古板的程序员不喜欢这个特性,认为它会影响到程序的可读性。但是实际情况恰恰相反,合理的利用lambda表达式,不仅不会污染代码的可读性,反而会大大加强可读性。lambda表达式这个特性,已经被现在很多新的编程语言吸收,足见其流行程度。

错误的测试方法

很多朋友可能对lambda表达式的运行速度产生疑问,会不会用了这种写法,程序的运行速度就会变慢呢?这种担心也是完全多余的,Java作为一门经典的企业级应用开发语言,Oracle对每个新添加的特性都是小心翼翼的。既然这个特性被添加到Java语言中,那么足以说明Oracle对其进行了深刻的优化,运行速度绝对是有保证的,就算比普通循环慢一点,也不会慢到哪里去。

可能有些人用了错误的测试方法对lambda表达式进行了测试,发现速度不如普通的for循环,然后就得出结论:lambda表达式运行速度慢。这种测试是完全不负责任的。下面的代码就是一种错误的测试方法,测试结果:lambda表达式用时150毫秒,而普通循环用时7毫秒。因此得出结论:lambda表达式慢。大家可以看看代码,然后想想问题在哪里。

public class LambdaTest {public static int N = 1_0000_0000; static List list = IntStream.range(0, N).boxed().collect(Collectors.toList()); public static void main(String[] args) { long start, end; start = System.currentTimeMillis(); lambdaTest(); end = System.currentTimeMillis(); System.out.println("lambda:" + (end - start)); start = System.currentTimeMillis(); loopTest(); end = System.currentTimeMillis(); System.out.println("loop:" + (end - start)); } static void lambdaTest() { list.forEach(i -> { }); } static void loopTest() { for (int i = 0; i < list.size(); i++) { } }}好了不卖关子了,直接说结论吧。上面测试方法的问题在于,两种测试方法实际上根本不对等。lambda表达式的测试中,虽然方法体是空的,但是程序执行的时候,仍然会取出每一个元素,然后再应用空的方法。而循环测试中,真的只是执行了一个空循环,什么也没干。因此这种方法测出来的结论,完全不能证明lambda表达式比空循环慢。

公平的测试方法应该是怎么样的呢?对于循环,一样要加上取元素和应用空方法的操作。为此在空循环中增加了一部分代码。这样测出来的结果,lambda表达式和普通循环一样都是150毫秒左右,存在几毫秒的误差。这次的结果可以反映真实情况了,那就是两者没有什么速度差别。大家可以自己运行代码试试。

更加实际的测试

不管怎么说,用空的方法来测试lambda表达式和普通循环并不具有实际意义。所以我换了一种更加实际的方法,来看看lambda相较于普通的循环有没有优势所在。

首先准备一个用户类,这里用到了lombok自动生成各种工具方法,为我们节约时间。

然后准备一个随机类,准备用来生成10万个随机用户,来进行下一步的操作。

接下来就是测试代码了。测试代码其实也很简单,随机生成一千万个用户,然后进行简单的筛选操作,选出来所有ID大于1000且为偶数,用户名以字母a开头的用户。两种测试结果输出各自的筛选结果数量,以保证结果是相同的。因为这次的测试比较复杂,所以可以看出实际的差异。在我的电脑上,lambda表达式耗时100毫秒左右,而循环耗时80毫秒左右。可见lambda表达式虽然比循环慢一点,但是差距很小,在千万次循环的级别仅差几十毫秒,对程序的运行基本没有什么影响。

public class LambdaTest { public static int N = 1000_0000; static List list; public static void main(String[] args) { init(); long start, end; start = System.currentTimeMillis(); lambdaTest(); end = System.currentTimeMillis(); System.out.println("lambda:" + (end - start)); start = System.currentTimeMillis(); loopTest(); end = System.currentTimeMillis(); System.out.println("loop:" + (end - start)); } static void init() { list = new ArrayList<>(); for (int i = 0; i < N; i++) { list.add(new User(MyRandom.randomId(), MyRandom.randomUsername())); } } static void lambdaTest() { List r = list.stream() .filter(e -> e.getName().startsWith("a")) .filter(e -> e.getId() % 2 == 0) .filter(e -> e.getId() > 1000) .collect(Collectors.toList()); System.out.println(r.size()); } static void loopTest() { List r = new ArrayList<>(); for (User user : list) { if (user.getName().startsWith("a") && user.getId() % 2 == 0 && user.getId() > 1000) { r.add(user); } } System.out.println(r.size()); }}这次的测试算是一个比较实际的测试了,生成一千万个用户并对其属性进行检查,过滤出符合条件的用户。测试的数量是一千万,但是测试结果相差并不大。可见其实lambda表达式并不怎么影响程序的运行速度。值得注意的是,这个测试数据完全是保存在内存上的,而一般情况下数据都是从数据库中加载出来的。这时候程序的瓶颈在数据库的IO上,就算程序本身速度相差几十毫秒,相较于数据库的延迟完全可以忽略不计。

我们的原则是不进行过早的优化。写程序的时候,该怎么写就怎么写,lambda这种好用的新特性,该用的时候就应该用,不要害怕它影响性能。等到程序写完,需要优化的时候,老老实实的跑profile,查看程序的瓶颈究竟在哪里。一般情况下程序问题都在数据库IO、算法不够高效或者是内存泄露上,我还真没听说过哪个程序写的非常完美,就是被lambda表达式的速度拖后腿的。实际上,虽然很多程序员都担心lambda表达式的速度,但是他们的程序完全优秀到需要扣lambda表达式细节的这种程度。

反过来说适当的时候应用这些新特性,反而会增加代码的可读性。就拿上面这个例子来说,通过三次filter方法过滤程序,最后用collect方法得到结果,这种流式函数调用不仅非常简单易读,而且十分优雅。反观循环版本中的查找操作,只能通过if判断简单粗暴的进行。这还是一个简单的例子,假如查找操作比较复杂,带了十几个查询条件的话,那么循环版本的代码就会变成可读性的灾难。

这里还有一个细节值得注意。为了最高效的运行,循环版本的代码只能在一个if中不断的增加判断条件。而lambda表达式版本则是流式调用了三次filter语句,但是它们的运行结果相差不大。相信你应该也猜到原因了:lambda表达式和流类库内部做了特殊的优化,就算是多个过滤条件,也会保证仅仅循环一次。因此放心大胆的使用lambda表达式吧!它是编写代码的利器!

lambda表达式,更加强大

写到这里,本文的内容应该是差不多了。但是我猜很多朋友看了以后,会说“你说了这么多,lambda表达式不还是比循环慢嘛。说来说去,我还是要继续用循环”。在这里我想说明一下,我的观点是:lambda表达式虽然比循环慢那么一点点,但是带来的便利性和优化空间,远远不是普通循环可以比拟的。

上面的例子用了一千万次的循环,才得到了几十毫秒的差距。而实际情况中,几千次或者几万次的循环,差距便会忽略不计。而且如果加上数据库等外部数据源的读写延迟,程序的这点运行速度完全就不值一提了。所有担心lambda表达式的朋友基本都是杞人忧天。而lambda表达式带来的方便确实实实在在的。更重要的是,普通循环的优化非常困难,基本要重写整个代码,在这之中很容易发生错误。而lambda表达式的优化则简单许多。

上面的例子恰好是一个适合并行化的例子,优化方法很简单,多加一行parallel()方法调用即可。并行化是另外一个非常复杂的主题,但是在这个例子中,第一数据量大(一千万之多),第二数据易于分割和和合并(ArrayList可以用下标直接定位中间的元素),第三操作都是只读的(不会影响到数据集本身),所以正好适合并行化。并行化之后,lambda表达式的运行速度已经和循环相差无几了(仅差几毫秒左右)。而普通代码的并行化,我想这就不是一般程序员可以轻松写出来的东西了。

好了,本文终于写完了,其实本来准备反驳一下错误观点就结束的,结果不知不觉写了这么多。如果大家觉得本文不错的话,欢迎点赞、评论、转发,创作不易,还请大家多多支持!在此先谢谢各位了。

阅读剩余内容
网友评论
相关内容
延伸阅读
小编推荐

大家都在看

关于书的作文结尾 家务事作文300字 写环保的作文300字 如何写介绍自己的作文 快乐的新年作文 父爱伴我成长作文600字 小学三年级童话故事作文 我的家乡作文450 设立一个节日作文 英语图表分析作文 东方作文怎么样 消息的作文 关于家庭趣事的作文 分享爱作文 描写绿萝的作文600字 森林音乐会作文300字 我是一只狼作文 五年级上册八单元作文 小学语文第四单元作文 最苦与最乐作文500字 如何保持健康英语作文150字 感恩老师的英语作文 创新作文300字 感恩妈妈400字作文 可爱的小动物作文200字 我助人为乐的作文300字 我心中的雷锋作文400字 介绍南瓜的作文 我最喜欢的小猫作文 2019高考语文作文