深入探索Java 8 Lambda表达式
作者:网络转载 发布时间:[ 2016/6/15 10:34:56 ] 推荐标签:测试开发技术 Java
2014年3月,Java 8发布,Lambda表达式作为一项重要的特性随之而来。或许现在你已经在使用Lambda表达式来书写简洁灵活的代码。比如,你可以使用Lambda表达式和新增的流相关的API,完成如下的大量数据的查询处理:
int total = invoices.stream().filter(inv -> inv.getMonth() ==
Month.JULY).mapToInt(Invoice::getAmount).sum();
上面的示例代码描述了如何从一打发票中计算出7月份的应付款总额。其中我们使用Lambda表达式过滤出7月份的发票,使用方法引用来提取出发票的金额。
到这里,你可能会对Java编译器和JVM内部如何处理Lambda表达式和方法引用比较好奇。可能会提出这样的问题,Lambda表达式会不会是匿名内部类的语法糖呢?毕竟上面的示例代码可以使用匿名内部类实现,将Lambda表达式的方法体实现移到匿名内部类对应的方法中即可,但是我们并不赞成这样做。如下为匿名内部类实现版本:
int total = invoices.stream().filter(new Predicate()
{
@Override
public boolean test(Invoice inv) {
return inv.getMonth() == Month.JULY;
}
}).mapToInt(new ToIntFunction()
{
@Override
public int applyAsInt(Invoice inv)
{
return inv.getAmount();
}
}).sum();
本文将会介绍为什么Java编译器没有采用内部类的形式处理Lambda表达式,并解密Lambda表达式和方法引用的内部实现。接着介绍字节码生成并简略分析Lambda表达式理论上的性能。后,我们将讨论一下实践中Lambda表达式的性能问题。
为什么匿名内部类不好?
实际上,匿名内部类存在着影响应用性能的问题。
首先,编译器会为每一个匿名内部类创建一个类文件。创建出来的类文件的名称通常按照这样的规则 ClassName符合和数字。生成如此多的文件会带来问题,因为类在使用之前需要加载类文件并进行验证,这个过程则会影响应用的启动性能。类文件的加载很有可能是一个耗时的操作,这其中包含了磁盘IO和解压JAR文件。
假设Lambda表达式翻译成匿名内部类,那么每一个Lambda表达式都会有一个对应的类文件。随着匿名内部类进行加载,其必然要占用JVM中的元空间(从Java 8开始代的一种替代实现)。如果匿名内部类的方法被JIT编译成机器代码,则会存储到代码缓存中。同时,匿名内部类都需要实例化成独立的对象。以上关于匿名内部类的种种会使得应用的内存占用增加。因此我们有必要引入新的缓存机制减少过多的内存占用,这也意味着我们需要引入某种抽象层。
重要的,一旦Lambda表达式使用了匿名内部类实现,会限制了后续Lambda表达式实现的更改,降低了其随着JVM改进而改进的能力。
我们看一下下面的这段代码:
import java.util.function.Function;public class AnonymousClassExample
{
Function format = new Function()
{
public String apply(String input)
{
return Character.toUpperCase(input.charAt(0)) + input.substring(1);
}
};
}
使用这个命令我们可以检查任何类文件生成的字节码
javap -c -v ClassName
示例中使用Function创建的匿名内部类对应的字节码如下:
0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V4: aload_0 5: new #2 // class AnonymousClassExample$18: dup 9: aload_0 10: invokespecial #3 // Method AnonymousClass$1."":(LAnonymousClassExample;)V13: putfield #4 // Field format:Ljava/util/function/Function;16: return
上述字节码的含义如下:
第5行,使用字节码操作new创建了类型AnonymousClassExample$1的一个对象,同时将新创建的对象的的引用压入栈中。
第8行,使用dup操作复制栈上的引用。
第10行,上面的复制的引用被指令invokespecial消耗使用,用来初始化匿名内部类实例。
第13行,栈顶依旧是创建的对象的引用,这个引用通过putfield指令保存到AnonymousClassExample类的format属性中。
AnonymousClassExample1这个类文件,你会发现这个类是Function接口的实现。
将Lambda表达式翻译成匿名内部类会限制以后可能进行的优化(比如缓存)。因为一旦使用了翻译成匿名内部类形式,那么Lambda表达式则和匿名内部类的字节码生成机制绑定。因而,Java语言和JVM工程师需要设计一个稳定并且具有足够信息的二进制表示形式来支持以后的JVM实现策略。下面的部分将介绍不使用匿名内部类机制,Lambda表达式是如何工作的。
Lambdas表达式和invokedynamic
为了解决前面提到的担心,Java语言和JVM工程师决定将翻译策略推迟到运行时。利用Java 7引入的invokedynamic字节码指令我们可以高效地完成这一实现。将Lambda表达式转化成字节码只需要如下两步:
1.生成一个invokedynamic调用点,也叫做Lambda工厂。当调用时返回一个Lambda表达式转化成的函数式接口实例。
2.将Lambda表达式的方法体转换成方法供invokedynamic指令调用。
为了阐明上述的第一步,我们这里举一个包含Lambda表达式的简单类:
import java.util.function.Function;public class Lambda { Function f = s -> Integer.parseInt(s);}
查看上面的类经过编译之后生成的字节码:
0: aload_01: invokespecial #1 // Method java/lang/Object."":()V4: aload_05: invokedynamic #2, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;10: putfield #3 // Field f:Ljava/util/function/Function;13: return
需要注意的是,方法引用的编译稍微有点不同,因为javac不需要创建一个合成的方法,javac可以直接访问该方法。
Lambda表达式转化成字节码的第二步取决于Lambda表达式是否为对变量捕获。Lambda表达式方法体需要访问外部的变量则为对变量捕获,反之则为对变量不捕获。
对于不进行变量捕获的Lambda表达式,其方法体实现会被提取到一个与之具有相同签名的静态方法中,这个静态方法和Lambda表达式位于同一个类中。比如上面的那段Lambda表达式会被提取成类似这样的方法:
static Integer lambda$1(String s) { return Integer.parseInt(s);}
需要注意的是,这里的$1并不是代表内部类,这里仅仅是为了展示编译后的代码而已。
对于捕获变量的Lambda表达式情况有点复杂,同前面一样Lambda表达式依然会被提取到一个静态方法中,不同的是被捕获的变量同正常的参数一样传入到这个方法中。在本例中,采用通用的翻译策略预先将被捕获的变量作为额外的参数传入方法中。比如下面的示例代码:
int offset = 100;Function f = s -> Integer.parseInt(s) + offset;
对应的翻译后的实现方法为:
static Integer lambda$1(int offset, String s) { return Integer.parseInt(s) + offset;}
需要注意的是编译器对于Lambda表达式的翻译策略并非固定的,因为这样invokedynamic可以使编译器在后期使用不同的翻译实现策略。比如,被捕获的变量可以放入数组中。如果Lambda表达式用到了类的实例的属性,其对应生成的方法可以是实例方法,而不是静态方法,这样可以避免传入多余的参数。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11