12.10 Lambda表达式和匿名类
对很多开发者而言,Java 8 最激动人心的特性就是加入了 Lambda 表达式。不可否认,Lambda 对 Java 开发者的开发效率有着非常积极的影响,尽管收益难以量化,但是我们可以使用 Lambda 表达式来考查代码的性能。
关于 Lambda 表达式的性能,一个最基本的问题是,它们与其所对应的替代物匿名类相比如何。其实几乎没什么差别。关于如何使用 Lambda 表达式,常见的例子一般是从创建匿名内部类的代码入手(不过这类例子往往使用 Stream,而不是像下面这样使用迭代器;12.11 节会介绍 Stream 类):
private volatile int sum;public interface IntegerInterface {int getInt();}public void calc() {IntegerInterface a1 = new IntegerInterface() {public int getInt() {return 1;}};IntegerInterface a2 = new IntegerInterface() {public int getInt() {return 2;}};IntegerInterface a3 = new IntegerInterface() {public int getInt() {return 3;}};sum = a1.get() + a2.get() + a3.get();}
可以将其与下面使用了 Lambda 表达式的代码对比一下:
public void calc() {IntegerInterface a3 -> { return 3 };IntegerInterface a2 -> { return 2 };IntegerInterface a1 -> { return 1 };sum = a3.get() + a2.get() + a1.get();}
这里 Lambda 表达式或匿名类的代码体至关重要:如果其中执行了任何较为重型的操作,那花在这一操作上的时间会把 Lambda 表达式或匿名类实现上的细微差距掩盖掉。然而,即便在这种最简单的情况下,执行该操作的时间也基本一样,如表 12-8 所示。
表12-8:使用Lambda表达式和匿名类执行calc()方法的时间
| 实现方式 | 所用时间(微秒) |
|---|---|
| 匿名类 | 87.2 |
| Lambda 表达式 | 87.9 |
数字看上去比较正式,让人印象深刻,但除了说这两种实现性能基本相同,我们也得不出其他结论。确实如此,因为测试中存在随机波动,再加上这些调用都是用 System.nanoTime() 测量的。在这个层次上,这样计时还没有准确到足以让人信服;总而言之,我们所知道的就是它们的性能相同。
在这个例子中的典型用法中,有一点比较有趣,即每当方法被调用时,使用匿名类的代码都会创建一个新对象。如果这个方法调用次数非常多(当然必须在某个基准测试中测量其性能),会有很多这个匿名类的对象被快速创建并丢弃。如第 5 章所介绍,这种用法对性能几乎没有什么影响。分配对象(以及更重要的初始化操作)的成本非常低,而且因为它们很快就会被丢弃,实际上不会拖慢垃圾收集器。
尽管如此,我们总是可以构造一些用例,来说明分配对性能影响很大,以及最好重用对象:
private IntegerInterface a1 = new IntegerInterface() {public int getInt() {return 1;}};…… 其他接口类似……public void calc() {return a1.get() + a2.get() + a3.get();}}
而 Lambda 表达式的这种典型用法,通常不会在每次循环迭代时创建一个新对象,所以在个别案例下,使用 Lambda 表达式的性能会好一些。尽管如此,即便要构造性能差异有影响的微基准测试,都是非常困难的。
Lambda表达式与匿名类加载
有种极端情况,即在启动和类加载时,两种实现的性能差别很明显。人们很容易查看 Lambda 表达式的代码,并断定它不过是语法糖,底层还是创建匿名类(特别是从长远来看,两者的性能一样)。但现在的工作方式并不是这样的。在 JDK 8 中,Lambda 表达式的代码会创建一个静态方法,这个方法通过一个特殊的辅助类来调用。而匿名类是一个真正的 Java 类,有单独的 class 文件,并通过类加载器加载。
如本章前面所介绍的,类加载的性能可能很重要,特别是在 classpath 很长的情况下。如果这个例子就是在这样的情况下运行——calc() 方法每次都在一个新的类加载器中执行,那匿名类实现就处于劣势了。表 12-9 列出了这种情况下的差别。
表12-9:在一个新的类加载器中执行calc()方法的时间
| 实现方式 | 所用时间(微秒) |
|---|---|
| 匿名类 | 267 |
| Lambda 表达式 | 181 |
关于这些数字,有一点要提一下:它们都是在经过一段适当的热身周期(以开启编译)之后再测量的。但是在热身阶段会发生另一件事:class 文件第一次被从磁盘读取出来。操作系统会把这些文件保存在内存(操作系统的文件缓冲区)中。所以代码第一次执行需要的时间比较长,因为要通过读文件的系统调用把文件从磁盘中真正地加载进来。随后的调用会快很多:尽管仍然需要通过系统调用读文件,但因为这些文件已经在操作系统的内存中,所以数据可以快速返回。因此,匿名类实现的性能可能要比想象中好,因为它并没有真正地从磁盘读取 class 文件。
快速小结
1. 如果要在 Lambda 表达式和匿名类之间做出选择,则应该从方便编程的角度出发,因为性能上没什么差别。
2. Lambda 表达式并没有实现为类,所以有个例外情况,即当类加载行为对性能影响很大时,Lambda 表达式略胜一筹。
快速小结