java8 lambda表达式原理
java8已经推出有一段时间了,相信有不少公司已经把jdk升级到8了,每次jdk的升级都会带来一些性能以及应用上的优化,比如8移出了永久区,java.lang.OutOfMemoryError: PermGen space离我们而去,以及一些新的语法糖lambda,stream,默认方法等等,本文就来说说lambda表达式
lambda表达式写法
基本语法:
(parameters) -> expression
或
(parameters) ->{ statements; }
左边为传入参数,右边为执行代码,代表一个函数式接口的实现
lambda例子
此处不多写,贴出其他人的博客,请见Java8 lambda表达式10个示例
lambda表达式与旧的api对比
- 对比 code
public class LambdaTest {
public static void main(String[] args) {
ArrayList<Integer> integers = new ArrayList<Integer>() ;
integers.add(9);
integers.add(8);
integers.add(7);
integers.add(6);
integers.sort(new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("匿名内部类排序输出:"+integers);
integers.sort((o1,o2)->o1.compareTo(02));
System.out.println("lambda1表达式排序输出:"+integers);
integers.sort(Integer::compareTo);
System.out.println("lambda2表达式排序输出:"+integers);
}
}
- 对比 输出
匿名内部类排序输出:[6, 7, 8, 9]
lambda1表达式排序输出:[6, 7, 8, 9]
lambda2表达式排序输出:[6, 7, 8, 9]
- 编译后的文件
从上面看到,生成了两个class文件,一个是LambdaTest类的class文件,一个是Comparator匿名内部类的class文件,lambda表达式并未生成匿名内部类class文件,也就是说java8并不是靠编译器将lambda转换为匿名内部类,lambda脱糖过程稍后描述。
lambda原理
在java8中每一个Lambda表达式必须有一个函数式接口与之对应,那么函数式接口是什么呢?
什么是函数式接口
函数式接口(functional interface)简单来说就是只包含一个抽象方法的普通接口,java.lang.Runnable、java.util.Comparator都是函数式接口,java8提供了java.lang.FunctionalInterface注解进行标准,但是是非必须的,jdk会自动识别函数式接口。函数式接口可以被隐式转换为lambda表达式。
函数式与lambda表达式关系实例
- 正确实例
public class LambdaTest2 {
public interface TestInterface{
public void test1();
}
public static void doSomething(TestInterface test){
test.test1();
}
public static void main(String[] args) {
doSomething(()-> System.out.println("HelloWorld"));
}
}
输出HelloWorld
- 错误实例
public class LambdaTest3 {
public interface TestInterface{
public void test1();
public void test2(int a);
}
public static void doSomething(TestInterface test){
test.test1();
}
public static void main(String[] args) {
doSomething(()-> System.out.println("HelloWorld"));
}
}
编译错误:
Error:(19, 21) java: 不兼容的类型: com.chen.LambdaTest3.TestInterface 不是函数接口
在 接口 com.chen.LambdaTest3.TestInterface 中找到多个非覆盖抽象方法
那么lambda是怎么跟函数式接口对应的呢
来看一下LambdaTest2的字节码
Classfile /F:/workspace/java8-test/target/classes/com/chen/LambdaTest2.class
Last modified 2017-2-18; size 1376 bytes
MD5 checksum 5de06d632caf9ead069b79cf854411b9
Compiled from "LambdaTest2.java"
public class com.chen.LambdaTest2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#31 // java/lang/Object."<init>":()V
#2 = InterfaceMethodref #10.#32 // com/chen/LambdaTest2$TestInterface.test1:()V
#3 = InvokeDynamic #0:#37 // #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
#4 = Methodref #8.#38 // com/chen/LambdaTest2.doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
#5 = Fieldref #39.#40 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #41 // HelloWorld
#7 = Methodref #42.#43 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #44 // com/chen/LambdaTest2
#9 = Class #45 // java/lang/Object
#10 = Class #46 // com/chen/LambdaTest2$TestInterface
#11 = Utf8 TestInterface
#12 = Utf8 InnerClasses
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/chen/LambdaTest2;
#20 = Utf8 doSomething
#21 = Utf8 (Lcom/chen/LambdaTest2$TestInterface;)V
#22 = Utf8 test
#23 = Utf8 Lcom/chen/LambdaTest2$TestInterface;
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 args
#27 = Utf8 [Ljava/lang/String;
#28 = Utf8 lambda$main$0
#29 = Utf8 SourceFile
#30 = Utf8 LambdaTest2.java
#31 = NameAndType #13:#14 // "<init>":()V
#32 = NameAndType #47:#14 // test1:()V
#33 = Utf8 BootstrapMethods
#34 = MethodHandle #6:#48 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#35 = MethodType #14 // ()V
#36 = MethodHandle #6:#49 // invokestatic com/chen/LambdaTest2.lambda$main$0:()V
#37 = NameAndType #47:#50 // test1:()Lcom/chen/LambdaTest2$TestInterface;
#38 = NameAndType #20:#21 // doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
#39 = Class #51 // java/lang/System
#40 = NameAndType #52:#53 // out:Ljava/io/PrintStream;
#41 = Utf8 HelloWorld
#42 = Class #54 // java/io/PrintStream
#43 = NameAndType #55:#56 // println:(Ljava/lang/String;)V
#44 = Utf8 com/chen/LambdaTest2
#45 = Utf8 java/lang/Object
#46 = Utf8 com/chen/LambdaTest2$TestInterface
#47 = Utf8 test1
#48 = Methodref #57.#58 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#49 = Methodref #8.#59 // com/chen/LambdaTest2.lambda$main$0:()V
#50 = Utf8 ()Lcom/chen/LambdaTest2$TestInterface;
#51 = Utf8 java/lang/System
#52 = Utf8 out
#53 = Utf8 Ljava/io/PrintStream;
#54 = Utf8 java/io/PrintStream
#55 = Utf8 println
#56 = Utf8 (Ljava/lang/String;)V
#57 = Class #60 // java/lang/invoke/LambdaMetafactory
#58 = NameAndType #61:#64 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#59 = NameAndType #28:#14 // lambda$main$0:()V
#60 = Utf8 java/lang/invoke/LambdaMetafactory
#61 = Utf8 metafactory
#62 = Class #66 // java/lang/invoke/MethodHandles$Lookup
#63 = Utf8 Lookup
#64 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#65 = Class #67 // java/lang/invoke/MethodHandles
#66 = Utf8 java/lang/invoke/MethodHandles$Lookup
#67 = Utf8 java/lang/invoke/MethodHandles
{
public com.chen.LambdaTest2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/chen/LambdaTest2;
public static void doSomething(com.chen.LambdaTest2$TestInterface);
descriptor: (Lcom/chen/LambdaTest2$TestInterface;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokeinterface #2, 1 // InterfaceMethod com/chen/LambdaTest2$TestInterface.test1:()V
6: return
LineNumberTable:
line 15: 0
line 16: 6
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 test Lcom/chen/LambdaTest2$TestInterface;
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokedynamic #3, 0 // InvokeDynamic #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
5: invokestatic #4 // Method doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "LambdaTest2.java"
InnerClasses:
public static #11= #10 of #8; //TestInterface=class com/chen/LambdaTest2$TestInterface of class com/chen/LambdaTest2
public static final #63= #62 of #65; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#35 ()V
#36 invokestatic com/chen/LambdaTest2.lambda$main$0:()V
#35 ()V
其中main方法
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokedynamic #3, 0 // InvokeDynamic #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
5: invokestatic #4 // Method doSomething:(Lcom/chen/LambdaTest2$TestInterface;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
执行了三条指令 invokedynamic、invokestatic、return
- 第一条是lambda表达式转化为函数式接口TestInterface
- 第二条执行doSomething方法
- 第三条退出main方法
invokedynamic指令是在jvm7中新增的,invokedynamic出现的位置代表一个动态调用点
invokedynamic指令后面会跟一个指向常量池的调用点限定符,这个限定符会被解析为一个动态调用点。
调用点限定符的符号引用为CONSTANT_InvokeDynamic_info结构
CONSTANT_InvokeDynamic_info{
u1 tag;
u2 bootstrap_method_attr_index;
u2 name_and_type_index;
}
依据这个可以找到对应的动态调用引导方法Java.lang.invoke.CallSite
此处invokedynamic后面跟的是常量#3,#3指向#0和#37,#37代表TestInterface接口的test1方法
#3 = InvokeDynamic #0:#37 // #0:test1:()Lcom/chen/LambdaTest2$TestInterface;
#37 = NameAndType #47:#50 // test1:()Lcom/chen/LambdaTest2$TestInterface;
而#0在字节码最后的BootstrapMethods中,Method arguments#36代表这个lambda表达式调用代码
BootstrapMethods:
0: #34 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#35 ()V
#36 invokestatic com/chen/LambdaTest2.lambda$main$0:()V
#35 ()V
根据BootstrapMethods对应的#34可以找到此处lambda InvokeDynamic指令对应的引导方法是LambdaMetafactory.metafactory,其返还一个CallSite
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType)
throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
观察源码可得,其通过new一个InnerClassLambdaMetafactory并调用buildCallSite方法创造了类似内部类的lambda CallSite
有兴趣可以看buildCallSite方法的源码
InnerClassLambdaMetafactory类的源码注释是
Lambda metafactory implementation which dynamically creates an inner-class-like class per lambda callsite.
至此,lambda表达式的脱糖过程已经了解完成,其中很多细节设计太多java字节码知识,本汪也不是太了解,有兴趣可以多看看java虚拟机规范
至于为什么要绕一圈生成一个内部类的动态调用点然后执行,而不是直接把lambda编译成内部类,也许是为了减少编译后的文件数,具体不得而知,有待研究