Lambda原理及应用
Lambda介绍
Lambda 是 JDK8 以后版本推出的一个新特性,也是一个重要的版本更新,利用 Lambda 可以简化内部类,可以更方便的进行集合的运算,让你的代码看起来更加简洁,也能提升代码的运行效率。
Lambda语法
非静调用
(parameters...) -> expression
或者
(parameters...) ->{ statements; }
静态调用
(parameters...) -> Class.Method(parameters...)
通常也可以简写成
Class::Method
举个例子
() -> 1
i -> 2 * i
(String s) -> {System.out.print(s);}
(o1, o2) -> Integer.compare(o1, o2)
Integer::compare
实现 Lambda 的基础
MethodHandle
MethodHandle 和 Reflection 都可以分派方法调用,但是 MethodHandle 比 Reflection 更强大,它是模拟字节码层次的方法分派。
MethodHandle 是结合 invokedynamic 指令一起为动态语言服务的,也就是说 MethodHandle (更准确的来说是其设计理念)是服务于所有运行在JVM之上的语言,而 Relection 则只是适用 Java 语言本身。
例子
通过MethodHandle 实现方法调用,调用 String#substring。
package org.lin;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
public class MethodHandleTest {
public MethodHandle getHandler() throws NoSuchMethodException, IllegalAccessException {
//方法签名
MethodType mt = MethodType.methodType(String.class, int.class, int.class);
//上下文
MethodHandles.Lookup lk = MethodHandles.lookup();
//
MethodHandle mh = lk.findVirtual(String.class, "substring", mt);
return mh;
}
public static void main(String[] args) throws Throwable {
MethodHandle mh = new MethodHandleTest().getHandler();
String str = "hello world";
Object result1 = mh.invoke(str, 1, 3);
// Object result2 = (String) mh.invokeExact(str, 1, 3);
System.out.println("MethodHandle invoke: " + result1);
System.out.println("substring: " + str.substring(1,3));
}
}
运行结果:
MethodHandle invoke: el
substring: el
查看字节码:
// class version 60.0 (60)
// access flags 0x21
public class org/lin/MethodHandleTest {
// compiled from: MethodHandleTest.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0x1
public <init>()V
L0
LINENUMBER 8 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lorg/lin/MethodHandleTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public getHandler()Ljava/lang/invoke/MethodHandle; throws java/lang/NoSuchMethodException java/lang/IllegalAccessException
L0
LINENUMBER 13 L0
LDC Ljava/lang/String;.class
GETSTATIC java/lang/Integer.TYPE : Ljava/lang/Class;
ICONST_1
ANEWARRAY java/lang/Class
DUP
ICONST_0
GETSTATIC java/lang/Integer.TYPE : Ljava/lang/Class;
AASTORE
INVOKESTATIC java/lang/invoke/MethodType.methodType (Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;
ASTORE 1
L1
LINENUMBER 15 L1
INVOKESTATIC java/lang/invoke/MethodHandles.lookup ()Ljava/lang/invoke/MethodHandles$Lookup;
ASTORE 2
L2
LINENUMBER 17 L2
ALOAD 2
LDC Ljava/lang/String;.class
LDC "substring"
ALOAD 1
INVOKEVIRTUAL java/lang/invoke/MethodHandles$Lookup.findVirtual (Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;
ASTORE 3
L3
LINENUMBER 18 L3
ALOAD 3
ARETURN
L4
LOCALVARIABLE this Lorg/lin/MethodHandleTest; L0 L4 0
LOCALVARIABLE mt Ljava/lang/invoke/MethodType; L1 L4 1
LOCALVARIABLE lk Ljava/lang/invoke/MethodHandles$Lookup; L2 L4 2
LOCALVARIABLE mh Ljava/lang/invoke/MethodHandle; L3 L4 3
MAXSTACK = 6
MAXLOCALS = 4
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/lang/Throwable
L0
LINENUMBER 22 L0
NEW org/lin/MethodHandleTest
DUP
INVOKESPECIAL org/lin/MethodHandleTest.<init> ()V
INVOKEVIRTUAL org/lin/MethodHandleTest.getHandler ()Ljava/lang/invoke/MethodHandle;
ASTORE 1
L1
LINENUMBER 23 L1
LDC "hello world"
ASTORE 2
L2
LINENUMBER 24 L2
ALOAD 1
ALOAD 2
ICONST_1
ICONST_3
INVOKEVIRTUAL java/lang/invoke/MethodHandle.invoke (Ljava/lang/String;II)Ljava/lang/Object;
ASTORE 3
L3
LINENUMBER 26 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 3
INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/Object;)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
// arguments:
"MethodHandle invoke: \u0001"
]
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 27 L4
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
ALOAD 2
ICONST_1
ICONST_3
INVOKEVIRTUAL java/lang/String.substring (II)Ljava/lang/String;
INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
// arguments:
"substring: \u0001"
]
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L5
LINENUMBER 28 L5
RETURN
L6
LOCALVARIABLE args [Ljava/lang/String; L0 L6 0
LOCALVARIABLE mh Ljava/lang/invoke/MethodHandle; L1 L6 1
LOCALVARIABLE str Ljava/lang/String; L2 L6 2
LOCALVARIABLE result1 Ljava/lang/Object; L3 L6 3
MAXSTACK = 4
MAXLOCALS = 4
}
从字节码上也看到 MethodHandle 是通过 invokedynamic 指令实现的,基于指令实现,所以说是在 JVM 层级实现的。
Invokedynamic
Invokedynamic 是 JDK7 引入的一条新指令,通过一个调用点 CallSite 和 方法句柄 MethodHandle,来完成一个方法的调用。
CallSite 就是一个 MethodHandle 的 Holder,MethodHandle 指向一个调用点真正执行的方法。
Invokedynamic 是 JDK7 为了支持动态类型语言而新增的字节码指令,简单的理解,invokedynamic 在字节码层面实现了 MethodHandle 实现的功能。
对比之前 Java 字节码所有的方法分派均依赖于单纯的符号引用,即在编译生成的.class字节码文件中,4条方法调用指(invokevirtual、invokespecial、invokestatic、invokeinterface)后面跟的操作数均为明确的CONSTANT_Methodref_info 或 CONSTANT_InterfaceMethodref_info 常量池符号引用,这就意味着在编译期就提前把方法调用绑定到了某类型及其子类型。
例如:
System.out.println("substring: " + str.substring(1,3));
从字节码,可以看到 invokevirtual 编译期就绑定了对应的类和方法。
INVOKEVIRTUAL java/lang/String.substring (II)Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
Invokedynamic 最大的区别的它是一个动态绑定,在编译期没有进行类和方法的绑定,而是运行的时候动态解析和绑定。
LambdaMetafactory
LambdaMetafactory 是 JDK 内部 CallSite 和 MethodHandle 绑定实现类。
LambdaMetafactory的关键方法
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();
}
- MethodHandles.Lookup caller – 调用者,也就是 Invokedynamic 指令运行的上下文,通常可以通过 Lookup#lookupClass() 获取。
- String invokedName – Lambda 实现的接口的方法名称。
- MethodType invokedType – 调用点的方法签名描述 ,
- MethodType samMethodType – Lambda 实现的接口方法的签名描述 。(sam 就 single public abstract method 的缩写)
- MethodHandle implMethod – 生成的脱糖方法 desugaring Method签名。
- MethodType instantiatedMethodType – 对应 samMethodType 的运行时的方法签名,在非泛型的情况下于 samMethodType 相同。
VM Anonymous Class
VM Anonymous Class 内存级别匿名类,不需要 ClassLoader 加载,没有类名,当然也没其他权限管理等操作,这意味着效率更高(不必要的锁操作)、GC 更方便(没有 ClassLoader);
VM Anonymous Class 通过调用 sun.misc.Unsafe.defineAnonymousClass 生成。
LambdaMetafactory#metafactory 对应的实现 InnerClassLambdaMetafactory 会生成一个 VM Anonymous Class 实现方法调用。
Lambda的原理
例子
我们来看下面的例子,使用 Lambda 实现一个 Runable 线程。
package org.lin;
public class LambdaTest {
public static void main(String[] args) {
new Thread(() -> System.out.println("run")).start();
}
}
分析一下它的字节码:
// access flags 0x21
public class org/lin/LambdaTest {
// compiled from: LambdaTest.java
// access flags 0x19
public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lorg/lin/LambdaTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 6 L0
NEW java/lang/Thread
DUP
INVOKEDYNAMIC run()Ljava/lang/Runnable; [
// handle kind 0x6 : 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;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
org/lin/LambdaTest.lambda$main$0()V,
()V
]
INVOKESPECIAL java/lang/Thread.<init> (Ljava/lang/Runnable;)V
INVOKEVIRTUAL java/lang/Thread.start ()V
L1
LINENUMBER 7 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 3
MAXLOCALS = 1
// access flags 0x100A
private static synthetic lambda$main$0()V
L0
LINENUMBER 6 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "run"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
关键指令
INVOKEDYNAMIC run()Ljava/lang/Runnable; [
// handle kind 0x6 : 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;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
org/lin/LambdaTest.lambda$main$0()V,
()V
]
通过调用一个 INVOKEDYNAMIC 指令 ,将方法链接到这个方法上 org/lin/LambdaTest.lambda$main$0()V 。
通过 jclasslib 工具查看,我们看到多了一个内部类(VM Anonymous Class):
在运行时加上命令得到动态运行时的内部类 ,也就是 VM Anonymous Class 。
-Djdk.internal.lambda.dumpProxyClasses=<path>
生成的 VM Anonymous Class
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.lin;
// $FF: synthetic class
final class LambdaTest$$Lambda$1 implements Runnable {
private LambdaTest$$Lambda$1() {
}
public void run() {
LambdaTest.lambda$main$0();
}
}
该类 实现了 Runnable 接口,并在 run 方法上调用了 LambdaTest.lambda$main$0();
也就是上面字节码,动态生成的方法(desugaring method),该方法是经过 Lambda 语法,进行 脱糖分析 生成在字节码的内容。
// access flags 0x100A
private static synthetic lambda$main$0()V
L0
LINENUMBER 6 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "run"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
MAXSTACK = 2
MAXLOCALS = 0
原理总结
Lambda 利用 Invokedynamic 指令把调用和解析分离。
利用 invokedynamic 指令本身的特性(引导方法),把 Lambda 表达式在字节码文件的表示和真正的解析分离;
Lambda 表达式的链接解析发生运行期而非编译期,由 invokedynamic 的引导方法执行,扩展性强。
Lambda 是面向方法接口编程,利用 JDK8 提供的语法糖,因为对非方法引用的 Lambda 表达式,编译器都会为其生成一个方法实现 Lambda 表达式的逻辑,并出现在编译后的字节码文件中。这个方法的生成是 Lambda 表达式解析的关键,
也就是上面我们看到的 LambdaTest.lambda$main$0();* 方法。
当编译器遇到 lambda 表达式时,它首先将 lambda 主体脱糖(desugar)为一个方法,该方法的参数列表和返回类型与 lambda 表达式匹配,可能还有一些额外的参数(对于从词法范围捕获的值,如果有的话)。
在捕获 lambda 表达式的位置,它会生成一个invokedynamic callSite,调用时,会返回 lambda表达式转换为的 实现 Functional interface的实例。
此callSite 就是给出lambda实例 的 lambdaFactory, lambdaFactory 的动态参数是从词法范围捕获的值。启动 lambdaFactory 的方法是jdk的一个标准类 lambdaMetaFactory 提供的. 它在编译时获取当前 lambda 的已知信息并作为入参。
Lambda 运行的主要流程:
性能分析
从一份 oracle 官方的性能对比数据来看, Lambda 和匿名内部类性能对比。
Lambda 的 热启动效率比匿名内部类性能要稍微差一点,而 Lambda 的 冷启动效率比匿名内部类却高很多。
所谓热启动是包括生成匿名类字节码并生成 class 的过程。
对相同 Lambda 表达式来说,热启动过程只发生一次,后面都是调用工厂方法即可,直接调用已有的匿名类称之为冷启动。
由此可以得出,Lambda 的性能要比匿名内部类要高很多。
参考:
https://zhuanlan.zhihu.com/p/28124632
https://www.jianshu.com/p/d74e92f93752
https://www.jianshu.com/p/7ef49246c176
https://www.cnblogs.com/wzqshb/p/16987548.html
https://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf