Lambda表达式
简介:
Lambda表达式本质上是一种匿名方法,能将该方法作为参数传入其他方法中,用法上与一个只需实现一个方法的匿名内部类很相似,Lambda表达式能完全取代这种匿名内部类的位置,且拥有着更简洁的写法和更好的可读性。
用法:
// ( /*参数列表*/ )->{ /*具体实现*/ }
public class MyClass {
String s="OutString";
public static void main(String[] args){
// 比较常用的进程的定义
// 匿名内部类的写法
new Thread(new Runnable() {
@Override
public void run() {
//具体实现
s="change";
// 使用外部成员时,会隐式地将其指定为final类型,无法更改其值,故该部分错误
}
}).start();
// Lambda表达式
new Thread(()->{
//具体实现
}).start();
//需要参数的情况,在此定义一个Button button=new Button();
button.setOnClickListener((v)->{ //能识别形参的参数类型,多个参数用逗号隔开
//具体实现
});
test(()->s); //当只有一行代码时不需要“{}”,且能自动识别返回类型
}
public static void test(testClass s){}
}
interface testClass{
String m();
}
Lambda表达式与匿名类的区别:
如果只是想知道什么场合、怎么使用Lambda表达式,上面的部分就足够了,接下来将对Java是如何具体实现Lambda表达式和匿名内部类做出介绍,会发现,两者有着本质的不同。
静态类型语言与动态类型语言
Java是静态类型语言,举个栗子:
obj.println("Hello world");
显然,这行代码需要一个具体的上下文才有讨论的意义,假设它在Java语言中,并且变量obj的类型为java.io.PrintStream,那obj的值就必须是PrintStream的子类(实现了PrintStream接口的类)才是合法的。否则,哪怕obj拥有一个println(String)方法,但与PrintStream接口没有继承关系,代码依然不能运行——因为类型检查不合法。
但是相同的代码在动态类型语言ECMAScript(JavaScript)中情况则不一样,无论obj具体是何种类型,只要这种类型的定义中确实包含有println(String)方法,那方法调用便可成功。
这种差别产生的原因是Java语言在编译期间却已将println(String)方法完整的符号引用(本例中为一项CONSTANT_InterfaceMethodref_info常量)生成出来,作为方法调用指令的参数存储到Class文件中,例如下面这个例子:
public void print(){
PrintStream printStream=System.out;
printStream.println("Hello world");
}
//对应的.class文件,在命令行中使用“javap -c *.class”命令查看
public void print();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: astore_1
4: aload_1
5: ldc #3 // String Hello world
7: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
这个符号引用(Code的“7: invokevirtual ……”部分)包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息,通过这个符号引用,虚拟机就可以翻译出这个方法的直接引用(譬如方法内存地址或者其他实现形式)。而在ECMAScript等动态类型语言中,变量obj本身是没有类型的,变量obj的值才具有的类型,编译时候最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型(方法接收者不固定)。“变量无类型而变量值才有类型”这个特点也是动态类型语言的一个重要特征。
Java的动态语言实现
JDK 7时Java在虚拟机层面提供了动态类型的直接支持:invokedynamic和java.lang.invoke包,为Lambda表达式的出现奠定了基础。
在此对invokedynamic指令做简单介绍:每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法上。
invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V
//“#123”表示从常量池第123项获取常量(CONSTANT_InvokeDynamic_info常量)
//0表示BootstrapMethods属性表第0项
//其内容则是“testMethod:(Ljava/lang/String;)V”
Lambda表达式的本质
Java无法将方法作为参数传递给其他方法,因此大多数情况下会定义一个包含一个抽象方法的接口用来作为方法的载体。Lambda表达式弥补了这一点,Lambda表达式的本质是一个抽象方法,可以将其作为参数传递。
public class MyClass {
public void print (){
new Thread(()-> {}).start(); //使用Lambda表达式
}
}
//其对应的字节码
public class com.example.MyClass {
public com.example.MyClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void print();
Code:
0: new #2 // class java/lang/Thread
3: dup
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
9: invokespecial #4 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
12: invokevirtual #5 // Method java/lang/Thread.start:()V
15: return
}
4: invokedynamic #3, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
此为Lambda表达式对应的字节码,可以看到这里调用run方法,返回java.lang.Runnable类型的对象,然后以此构建Thread对象。
匿名内部类与Lambda表达式的对比
观察匿名内部类的实现:
public class MyClass {
public void print (){
new Thread(new Runnable() {
@Override
public void run() {}
}).start();
}
}
//MyClass.class其对应的字节码
public class com.example.MyClass {
public com.example.MyClass();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void print();
Code:
0: new #2 // class java/lang/Thread
3: dup
4: new #3 // class com/example/MyClass$1
7: dup
8: aload_0
9: invokespecial #4 // Method com/example/MyClass$1."<init>":(Lcom/example/MyClass;)V
12: invokespecial #5 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
15: invokevirtual #6 // Method java/lang/Thread.start:()V
18: return
}
//MyClass$1.class其对应的字节码
class com.example.MyClass$1 implements java.lang.Runnable {
final com.example.MyClass this$0;
com.example.MyClass$1(com.example.MyClass);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/MyClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
public void run();
Code:
0: return
}
从中可以看出,匿名内部类首先构造了一个MyClass$1类的对象(该类继承Runnable接口),然后使用该对象构造Thread对象,事实上,在MyClass.class文件所在的文件夹中同时存在MyClass$1.class文件。由此可看出,匿名内部类的实现就是由编译器构建一个继承对应接口的类。
由此可得知Lambda表达式是方法,匿名内部类是类,明白了这些,很多事情就简单了,如:两者如何接受外部类变量和外部方法变量?
Lambda以方法参数的形式接收:
public class MyClass {
String classString="Hellow"; //外部类变量
public void print (){
String functionString="World"; //外部方法变量
new Thread(()->{
String a=classString+functionString;
}).start();
}
}
//对应字节码
public com.example.MyClass();
Code:
……
public void print();
Code:
……
9: invokedynamic #6, 0 // InvokeDynamic #0:run:(Lcom/example/MyClass;Ljava/lang/String;)Ljava/lang/Runnable;
//run方法多了两个参数,一个指向外部类,一个指向外部方法中的String变量
14: invokespecial #7 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
17: invokevirtual #8 // Method java/lang/Thread.start:()V
20: return
}
内部类则是将变量添加入自身属性列表:
class com.example.MyClass$1 implements java.lang.Runnable {
final java.lang.String val$functionString;
//外部方法中的String变量对应的常量,内部使用时才存在
final com.example.MyClass this$0;
//指向MyClass的常量一直存在,不管内部有没有使用外部类的属性
com.example.MyClass$1(com.example.MyClass, java.lang.String);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/example/MyClass;
5: aload_0
6: aload_2
7: putfield #2 // Field val$functionString:Ljava/lang/String;
10: aload_0
11: invokespecial #3 // Method java/lang/Object."<init>":()V
14: return
public void run();
Code:
……
28: return
}
对比之前方法体为空的字节码可以看出,内部类会一直有一个指向外部类的指针,而Lambda表达式只有在使用到外部类的属性时,才会接收该参数。
Q&A:
Lambda表达式和匿名类均能直接访问外部方法的局部变量和外部类的属性,但访问时却隐式的指定所访问的成员是final(访问外部类的属性是通过指向外部类的指针完成的,该指针是final类型),为什么?
匿名内部类访问外部成员时,会将所访问的成员复制到类内),其原因是所访问的成员与匿名内部类生命周期的不同,当方法执行完毕后,局部变量就会被清理,但匿名类可能依旧存在,因此需要将其复制到内部。为了保证复制品与原始变量的一致,将其指定为final类型(Lambda表达式也是同样的道理)。JDK 8之后,检测条件宽松了许多,只要所使用的属性在之后的代码中,其值不会改变即可,不一定非要指定为final,但在JDK 8之前,外部方法中的局部变量需定义为final。
参考资料:
周志明.深入理解Java虚拟机——JVM高级特性与最佳实践