背景
不同于脚本语言可以直接调用函数,Java作为面向对象的语言需要提前创建类并实例化对象来调用实例方法,使用起来十分笨重。
比如我们需要构造一个如下ActionListener
接口的类:
public interface ActionListener { void actionPerformed(ActionEvent e);
}
需要重新写一套class TestActionListener implements ActionListener {...}
,如果我们有N种不同的实现,就需要写N次,很麻烦,lambda出现之前,可以使用匿名类的方式省去显式class的创建:
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("one")}
});
button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("two")}
});
但匿名类有以下缺点:
- 语法笨重
- 变量和this关键字指向不清晰
- Inflexible class-loading and instance-creation semantics
- Inability to capture non-final local variables
- Inability to abstract over control flow
关于变量和this关键字指向不清晰,可以看以下案例:
void caseOne() {String a = "a";Runnable runnable = new Runnable() {@Overridepublic void run() {System.out.println(a); // "a"}};runnable.run();}---void caseTwo() {String a = "a";Runnable runnable = new Runnable() {String a = "b";@Overridepublic void run() {System.out.println(this.a); // "b" this始终表示当前匿名类对象System.out.println(a); // "b" 匿名类中有变量a时,匿名类外同名变量失效}};runnable.run();}
lambda表达式的出现消除了第1和第2点,其中lambda中使用this关键字始终指向外部对象,这点和匿名类不一样。lambda避开了第3点的繁琐的类创建和实例化,第4点lambda增加了外部字段的final or effectively final检测,第4点和第5点问题并未被lambda表达式解决。
基本原理
我们创建一个lambda表达式并反编译看下bytecode:
import java.util.function.Function;public class TestLambda {public static void sayHelloWorld() {String world = "World";Function<String,String> func = (hello) -> hello + world;func.apply("hello");}
}
bytecode:
// class version 52.0 (52)
// access flags 0x21
public class TestLambda {// compiled from: TestLambda.java// access flags 0x19public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup// access flags 0x1public <init>()VL0LINENUMBER 3 L0ALOAD 0INVOKESPECIAL java/lang/Object.<init> ()VRETURNL1LOCALVARIABLE this LTestLambda; L0 L1 0MAXSTACK = 1MAXLOCALS = 1// access flags 0x9public static sayHelloWorld()VL0LINENUMBER 6 L0LDC "World"ASTORE 0L1LINENUMBER 7 L1ALOAD 0INVOKEDYNAMIC apply(Ljava/lang/String;)Ljava/util/function/Function; [// handle kind 0x6 : INVOKESTATICjava/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;// arguments:(Ljava/lang/Object;)Ljava/lang/Object;, // handle kind 0x6 : INVOKESTATICTestLambda.lambda$sayHelloWorld$0(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;, (Ljava/lang/String;)Ljava/lang/String;]ASTORE 1L2LINENUMBER 8 L2ALOAD 1LDC "hello"INVOKEINTERFACE java/util/function/Function.apply (Ljava/lang/Object;)Ljava/lang/Object; (itf)POPL3LINENUMBER 9 L3RETURNL4LOCALVARIABLE world Ljava/lang/String; L1 L4 0LOCALVARIABLE func Ljava/util/function/Function; L2 L4 1// signature Ljava/util/function/Function<Ljava/lang/String;Ljava/lang/String;>;// declaration: func extends java.util.function.Function<java.lang.String, java.lang.String>MAXSTACK = 2MAXLOCALS = 2// lambda中用到的外部变量作为方法参数传入,返回值类型为实现的接口方法的返回值类型// access flags 0x100Aprivate static synthetic lambda$sayHelloWorld$0(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;L0LINENUMBER 7 L0NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VALOAD 1INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;ALOAD 0INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;ARETURNL1LOCALVARIABLE world Ljava/lang/String; L0 L1 0LOCALVARIABLE hello Ljava/lang/String; L0 L1 1MAXSTACK = 2MAXLOCALS = 2
}
可以看到lambda表达式的产生了一个 invokedynamic + 一个静态方法 + 一个静态字段的字节码。
invokedynamic
在 Java7 之前,JVM 提供了如下 4 种【方法调用】指令:
invokestatic
: 调用静态方法,不会传递当前对象的引用,不会进行动态绑定invokevirtual
: 调用实例方法,传递当前对象的引用(this),使用vtable
进行动态绑定invokeinterface
: 调用接口的方法,使用itable
进行动态绑定invokespecial
: 调用特殊方法,如构造方法、私有方法、不会进行动态绑定
invokedynamic
: 用于处理新型的方法分派,允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,
比如,用户编写lambda表达式:() -> a.test(); b.test();
Jvm在执行该lambda表达式时,
会根据用户在lambda中编写的内容(即调用a和b的test方法)来执行。
注意,lambda表达式并不是invokedynamic
调用执行的,invokedynamic
调用只是生成了lambda方法对应的CallSite对象,具体执行还得靠invokeinterface
或invokevirtual
。关于为什么lambda使用dynamic可参考:Why are Java 8 lambdas invoked using invokedynamic?
实现上,invokedynamic
使用java.lang.invoke.LambdaMetafactory#metafactory
方法(也叫引导方法,Bootstrap method
)返回java.lang.invoke.CallSite
对象,CallSite
对象返回java.lang.invoke.MethodHandle
方法句柄并执行由lambda生成的静态方法。
以上都是Jvm的内部实现,用户代码其实只有一个lambda,我们使用Java代码来模拟下上面的调用方式,模拟之前先来了解一个名词duck typing
:
If it looks like a duck and quacks like a duck, it’s a duck
同理,Supplier
接口接收空参数,返回String类型,如果我们实现了一个类接收空参数,返回String类型,那它就实现了Supplier
接口,当然在Java这种强类型语言肯定有一个类型转换过程。
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Collections;
import java.util.function.Supplier;public class LambdaExpTest {public static String innerLambdaCode() {return "i'm lambda code";}// 等同于 Supplier<String> supplier = () -> "i'm lambda code";public static void mockLambdaInJvm() throws Throwable {MethodHandles.Lookup lookup = MethodHandles.lookup();// lambda实现的是Supplier接口,该方法需要接收lambda使用到的外部参数并返回一个Supplier,类似一个转换方法// The parameter types represent the types of capture variables; 入参是lambda使用到的外部参数类型// the return type is the interface to implement 出参是lambda实现的接口类型MethodType convertMethodType = MethodType.methodType(Supplier.class, Collections.emptyList());// Supplier的方法入参出参类型MethodType supplierMethodType = MethodType.methodType(Object.class, Collections.emptyList());CallSite callSite = LambdaMetafactory.metafactory(lookup,"get", // 调用的Supplier接口的方法名convertMethodType,supplierMethodType,// lambda内部逻辑委托给静态方法LambdaExpTest#innerLambdaCode执行lookup.findStatic(LambdaExpTest.class, "innerLambdaCode", MethodType.methodType(String.class)),supplierMethodType);MethodHandle factory = callSite.getTarget();Supplier<String> r = (Supplier<String>)factory.invoke();System.out.println(r.get());}public static void main(String[] args) throws Throwable {// 打印 i'm lambda codemockLambdaInJvm();}
}
参考
- why-are-java-8-lambdas-invoked-using-invokedynamic
- understanding-java-method-invocation-with-invokedynamic
- why-use-reflection-to-access-class-members-when-methodhandle-is-faster
- what-is-a-bootstrap-method
- whats-invokedynamic-and-how-do-i-use-it
- what-is-duck-typing
- VirtualCalls
- State of the Lambda
- An Introduction to Invoke Dynamic in the JVM
- 浅析 JVM invokedynamic 指令和 Java Lambda 语法|得物技术