关于java:Java8特性详解-lambda表达式三原理篇

7次阅读

共计 12789 个字符,预计需要花费 32 分钟才能阅读完成。

 Java 为什么须要 lambda 表达式?

可能晋升代码简洁性、进步代码可读性。

例如,在平时的开发过程中,把一个列表转换成另一个列表或 map 等等这样的转换操作是一种常见需要。
在没有 lambda 之前通常都是这样实现的。

List<Long> idList = Arrays.asList(1L, 2L, 3L);
List<Person> personList = new ArrayList<>();
for (long id : idList) {personList.add(getById(id));
}

代码反复多了之后,大家就会对这种常见代码进行形象,造成一些类库便于复用。
下面的需要能够形象成:对一个列表中的每个元素调用一个转换函数转换并输入后果列表。

interface Function {<T, R> R fun(T input);
}
<T, R> List<R> map(List<T> inputList, Function function) {List<R> mappedList = new ArrayList<>();
    for (T t : inputList) {mappedList.add(function.fun(t));
    }
    return mappedList;
}

有了这个形象,最开始的代码便能够”简化”成

List<Long> idList = Arrays.asList(1L, 2L, 3L);
List<Person> personList = map(idList, new Function<Long, Person>() {
    @Override
    public Person fun(Long input) {return getById(input);
    }
});

尽管实现逻辑少了一些,然而同样也遗憾地发现,代码行数还变多了。
因为 Java 语言中函数并不能作为参数传递到办法中 ,函数只能存放在一个类中示意。为了可能把函数作为参数传递到办法中,咱们被迫应用了匿名外部类实现,须要加相当多的冗余代码。
在一些反对函数式编程的语言 (Functional Programming Language) 中(例如 Python, Scala, Kotlin 等),函数是一等公民,函数能够成为参数传递以及作为返回值返回。
例如在 Kotlin 中,上述的代码能够缩减到很短,代码只蕴含要害内容,没有冗余信息。

val personList = idList.map {id -> getById(id) }

这样的编写效率差距也导致了一部分 Java 用户散失到其余语言,不过最终终于在 JDK8 也提供了 Lambda 表达式能力,来反对这种函数传递。

List<Person> personList = map(idList, input -> getById(input));

Lambda 表达式只是匿名外部类的语法糖吗?

如果要在 Java 语言中实现 lambda 表达式,初步察看,通过 javac 把这种箭头语法还原成匿名外部类,就能够轻松实现,因为它们性能根本是等价的(IDEA 中常常有提醒)。

然而匿名外部类有一些毛病。

  1. 每个匿名外部类都会在编译时创立一个对应的 class,并且是有文件的,因而在运行时不可避免的会有加载、验证、筹备、解析、初始化的类加载过程。
  2. 每次调用都会创立一个这个匿名外部类 class 的实例对象,无论是有状态的 (capturing,从上下文中捕捉一些变量)还是无状态(non-capturing) 的外部类。

invokedynamic 介绍

如果有一种函数援用、指针就好了,但 JVM 中并没有函数类型示意。
Java 中有示意函数援用的对象吗,反射中有个 Method 对象,但它的问题是性能问题,每次执行都会进行安全检查,且参数都是 Object 类型,须要 boxing 等等。

还有其余示意函数援用的办法吗?MethodHandle,它是在 JDK7 中与 invokedynamic 指令等一起提供的新个性。

但间接应用 MethodHandle 来实现,因为没有签名信息,会遇不能重载的问题。并且 MethodHandle 的 invoke 办法性能不肯定能保障比字节码调用好。

invokedynamic 呈现的背景

JVM 上的动静语言(JRuby, Scala 等),要实现 dynamic typing 动静类型,是比拟麻烦的。
这里简略解释一下什么是 dynamic typing,与其绝对的是 static typing 动态类型。
static typing: 所有变量的类型在编译时都是确定的,并且会进行类型查看。
dynamic typing: 变量的类型在编译时不能确定,只能在运行时能力确定、查看。

例如如下动静语言的例子,a 和 b 的类型都是未知的,因而 a.append(b)这个办法是什么也是未知的。

def add(val a, val b)
    a.append(b)

而在 Java 中 a 和 b 的类型在编译时就能确定。

SimpleString add(SimpleString a, SimpleString b) {return a.append(b);
}

编译后的字节码如下,通过 invokevirtual 明确调用变量 a 的函数签名为 (LSimpleString;)LSimpleString; 的办法。

0: aload_1
1: aload_2
2: invokevirtual #2 // Method SimpleString.append:(LSimpleString;)LSimpleString;
5: areturn

对于办法调用的字节码指令,JVM 中提供了四种。
invokestatic – 调用静态方法
invokeinterface – 调用接口办法
invokevirtual – 调用实例非接口办法的 public 办法
invokespecial – 其余的办法调用,private,constructor, super
这几种办法调用指令,在编译的时候就曾经明确指定了要调用什么样的办法,且均须要接管一个明确的常量池中的办法的符号援用,并进行类型查看,是不能轻易传一个不满足类型要求的对象来调用的,即便传过来的类型中也恰好有一样的办法签名也不行。

invokedynamic 性能

这个限度让 JVM 上的动静语言实现者感到很艰巨,只能临时通过性能较差的反射等形式实现动静类型。
这阐明在字节码层面无奈反对动静分派,该怎么办呢,又用到了大家相熟的”All problems in computer science can be solved by another level of indirection”了。
要实现动静分派,既然不能在编译时决定,那么咱们把这个决策推延到运行时再决定,由用户的自定义代码通知给 JVM 要执行什么办法。

在 jdk7,Java 提供了 invokedynamic 指令来解决这个问题,同时搭配的还有 java.lang.invoke 包。
这个指令大部分用户不太熟悉,因为不像 invokestatic 等指令,它在 Java 语言中并没有和它相干的间接概念。

要害的概念有如下几个

  1. invokedynamic 指令: 运行时 JVM 第一次到这里的时候会进行 linkage,会调用用户指定的 bootstrap method 来决定要执行什么办法,之后便不须要这个解析步骤。这个 invokedynamic 指令呈现的中央也叫做dynamic call site
  2. Bootstrap Method: 用户能够本人编写的办法,实现本人的逻辑最终返回一个 CallSite 对象。
  3. CallSite: 负责通过 getTarget()办法返回 MethodHandle
  4. MethodHandle: MethodHandle 示意的是要执行的办法的指针

再串联起来梳理下

invokedynamic在最开始时处于未链接 (unlinked) 状态,这时这个指令并不知道要调用的指标办法是什么。
当 JVM 要第一次执行某个中央的 invokedynamic 指令的时候,invokedynamic必须先进行链接 (linkage)。
链接过程通过调用一个 boostrap method,传入以后的调用相干信息,bootstrap method 会返回一个 CallSite,这个CallSite 中蕴含了 MethodHandle 的援用,也就是 CallSite 的 target。
invokedynamic指令便链接到这个 CallSite 上,并把所有的调用 delegate 到它以后的 targetMethodHandle上。依据 target 是否须要变换,CallSite能够分为 MutableCallSiteConstantCallSiteVolatileCallSite等,能够通过切换 target MethodHandle实现动静批改要调用的办法。

lambda 表达式真正是如何实现的

上面间接看一下目前 java 实现 lambda 的形式

以上面的代码为例

public class RunnableTest {void run() {
        Function<Integer, Integer> function = input -> input + 1;
        function.apply(1);
    }
}

编译后通过 javap 查看生成的字节码

void run();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=2, args_size=1
         0: invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/Function;
         5: astore_1
         6: aload_1
         7: iconst_1
         8: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        11: invokeinterface #4,  2            // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
        16: pop
        17: return
      LineNumberTable:
        line 12: 0
        line 13: 6
        line 14: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  this   Lcom/github/liuzhengyang/invokedyanmic/RunnableTest;
            6      12     1 function   Ljava/util/function/Function;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            6      12     1 function   Ljava/util/function/Function<Ljava/lang/Integer;Ljava/lang/Integer;>;

private static java.lang.Integer lambda$run$0(java.lang.Integer);
    descriptor: (Ljava/lang/Integer;)Ljava/lang/Integer;
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #5                  // Method java/lang/Integer.intValue:()I
         4: iconst_1
         5: iadd
         6: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
         9: areturn
      LineNumberTable:
        line 12: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0 input   Ljava/lang/Integer;

对应 Function<Integer, Integer> function = input -> input + 1; 这一行的字节码为

0: invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/Function;
5: astore_1

这里再温习一下 invokedynamic 的步骤。

  1. JVM 第一次解析时,调用用户定义的bootstrap method
  2. bootstrap method会返回一个CallSite
  3. CallSite中可能失去MethodHandle,示意办法指针
  4. JVM 之后调用这里就不再须要从新解析,间接绑定到这个 CallSite 上,调用对应的 target MethodHandle,并可能进行 inline 等调用优化

第一行 invokedynamic 前面有两个参数,第二个 0 没有意义固定为 0 第一个参数是 #2,指向的是常量池中类型为 CONSTANT_InvokeDynamic_info 的常量。

#2 = InvokeDynamic      #0:#32         // #0:apply:()Ljava/util/function/Function;

这个常量对应的 #0:#32 中第二个#32 示意的是这个 invokedynamic 指令对应的动静办法的名字和办法签名(办法类型)

#32 = NameAndType        #43:#44        // apply:()Ljava/util/function/Function;

第一个 #0 示意的是 bootstrap method 在 BootstrapMethods 表中的索引。在 javap 后果的最初看到是

BootstrapMethods:
  0: #28 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:
      #29 (Ljava/lang/Object;)Ljava/lang/Object;
      #30 invokestatic com/github/liuzhengyang/invokedyanmic/RunnableTest.lambda$run$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
      #31 (Ljava/lang/Integer;)Ljava/lang/Integer;

再看下 BootstrapMethods 属性对应 JVM 虚拟机标准里的阐明。

BootstrapMethods_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 num_bootstrap_methods;
    {   u2 bootstrap_method_ref;
        u2 num_bootstrap_arguments;
        u2 bootstrap_arguments[num_bootstrap_arguments];
    } bootstrap_methods[num_bootstrap_methods];
}

bootstrap_method_ref
The value of the bootstrap_method_ref item must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_MethodHandle_info structure

bootstrap_arguments[]
Each entry in the bootstrap_arguments array must be a valid index into the constant_pool table. The constant_pool entry at that index must be a CONSTANT_String_info, CONSTANT_Class_info, CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_MethodHandle_info, or CONSTANT_MethodType_info structure

CONSTANT_MethodHandle_info The CONSTANT_MethodHandle_info structure is used to represent a method handle

这个 BootstrapMethod 属性能够通知 invokedynamic 指令须要的 boostrap method 的援用以及参数的数量和类型。

28 对应的是 bootstrap_method_ref,为

#28 = MethodHandle       #6:#40         // 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;

依照 JVM 标准,BootstrapMethod 接管 3 个规范参数和一些自定义参数,规范参数如下

  1. MethodHandles.$Lookup类型的 caller 参数,这个对象可能通过相似反射的形式拿到在执行 invokedynamic 指令这个环境下可能调动到的办法,比方其余类的 private 办法是调用不到的。这个参数由 JVM 来入栈
  2. String 类型的 invokedName 参数,示意 invokedynamic 要实现的办法的名字,在这里是apply,是 lambda 表达式实现的办法名,这个参数由 JVM 来入栈
  3. MethodType 类型的 invokedType 参数,示意 invokedynamic 要实现的办法的类型,在这里是()Function,这个参数由 JVM 来入栈

29,#30,#31 是可选的自定义参数类型

#29 = MethodType         #41            //  (Ljava/lang/Object;)Ljava/lang/Object;
#30 = MethodHandle       #6:#42         // invokestatic com/github/liuzhengyang/invokedyanmic/RunnableTest.lambda$run$0:(Ljava/lang/Integer;)Ljava/lang/Integer;
#31 = MethodType         #21            //  (Ljava/lang/Integer;)Ljava/lang/Integer;

通过 java.lang.invoke.LambdaMetafactory#metafactory 的代码阐明下

public static CallSite metafactory(MethodHandles.Lookup caller,
        String invokedName,
        MethodType invokedType,
        MethodType samMethodType,
        MethodHandle implMethod,
        MethodType instantiatedMethodType)

后面三个介绍过了,剩下几个为
MethodType samMethodType: sam(SingleAbstractMethod) 就是#29 = MethodType #41 // (Ljava/lang/Object;)Ljava/lang/Object;,示意要实现的办法对象的类型,不过它没有泛型信息,(Ljava/lang/Object;)Ljava/lang/Object;
MethodHandle implMethod: 真正要执行的办法的地位,这里是com.github.liuzhengyang.invokedyanmic.Runnable.lambda$run$0(Integer)Integer/invokeStatic,这里是 javac 生成的一个对 lambda 解语法糖之后的办法,前面进行介绍
MethodType instantiatedMethodType: 和 samMethod 根本一样,不过会蕴含泛型信息,(Ljava/lang/Integer;)Ljava/lang/Integer;

private static java.lang.Integer lambda$run$0(java.lang.Integer);这个办法是有 javac 把 lambda 表达式 desugar 解语法糖生成的办法,如果 lambda 表达式用到了上下文变量,则为有状态的,这个表达式也叫做 capturing-lambda,会把变量作为这个生成办法的参数传进来,没有状态则为 non-capturing。
另外如果应用的是 java8 的 MethodReference,例如 Main::run 这种语法则阐明有能够间接调用的办法,就不须要再生成一个两头办法。

持续看 5: astore_1 这条指令,示意把以后操作数栈的对象援用保留到 index 为 1 的局部变量表中,即赋值给了 function 变量。
阐明后面执行完 invokedynamic #2, 0  后,在操作数栈中插入了一个类型为 Function 的对象。
这里的过程须要持续看一下 LambdaMetafactory#metafactory 的实现。

mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                        invokedName, samMethodType,
                                        implMethod, instantiatedMethodType,
                                        false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();

创立了一个 InnerClassLambdaMetafactory,而后调用buildCallSite 返回 CallSite

看一下 InnerClassLambdaMetafactory 是做什么的: Lambda metafactory implementation which dynamically creates an inner-class-like class per lambda callsite.

怎么回事!饶了一大圈还是创立了一个 inner class!先不要慌,先看完,最初剖析下和一般 inner class 的区别。

创立 InnerClassLambdaMetafactory 的过程大略是参数的一些赋值和初始化等
再看buildCallSite,这个简单一些,办法形容阐明为Build the CallSite. Generate a class file which implements the functional interface, define the class, if there are no parameters create an instance of the class which the CallSite will return, otherwise, generate handles which will call the class' constructor.

创立一个实现 functional interface 的的 class 文件,define 这个 class,如果是没有参数 non-capturing 类型的创立一个类实例,CallSite 能够固定返回这个实例,否则有状态,CallSite 每次都要通过构造函数来生成新对象。
这里相比一般的 InnerClass,有一个内存优化,无状态就应用一个对象。

办法实现的第一步是调用 spinInnerClass(),通过 ASM 生成一个 function interface 的实现类字节码并且进行类加载返回。

只保留要害代码
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC, lambdaClassName, null, JAVA_LANG_OBJECT, interfaces);
for (int i = 0; i < argDescs.length; i++) {FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL, argNames[i], argDescs[i], null, null);
    fv.visitEnd();}
generateConstructor();
if (invokedType.parameterCount() != 0) {generateFactory();
}
// Forward the SAM method
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName, samMethodType.toMethodDescriptorString(), null, null);
mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
new ForwardingMethodGenerator(mv).generate(samMethodType);

byte[] classBytes = cw.toByteArray();

return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);

生成办法为

  1. 申明要实现的接口
  2. 创立保留参数用的各个字段
  3. 生成构造函数,如果有参数,则生成一个 static Factory 办法
  4. 实现 function interface 里的要实现的办法,forward 到 implMethodName 上,也就是 javac 生成的办法或者 MethodReference 指向的办法
  5. 生成结束,通过 ClassWrite.toByteArray 拿到 class 字节码数组
  6. 通过 UNSAFE.defineAnonymousClass(targetClass, classBytes, null) define 这个外部类 class。这里的 defineAnonymousClass 比拟非凡,它创立进去的匿名类会挂载到 targetClass 这个宿主类上,而后能够用宿主类的类加载器加载这个类。然而不会然而并不会放到 SystemDirectory 里,SystemDirectory 是类加载器对象 + 类名字到 kclass 地址的映射,没有放到这个 Directory 里,就能够反复加载了,来不便实现一些动静语言的性能,并且可能避免一些内存泄露状况。

这些比拟形象,直观的看一下生成的后果

// $FF: synthetic class
final class RunnableTest$Lambda$1 implements Function {private RunnableTest$Lambda$1() { }

    @Hidden
    public Object apply(Object var1) {return RunnableTest.lambda$run$0((Integer)var1);
    }
}

如果有参数的状况呢,例如从外部类中应用了一个非动态字段,并应用了一个内部局部变量

private int a;
void run() {
    int b = 0;
    Function<Integer, Integer> function = input -> input + 1 + a + b;
    function.apply(1);
}

对应的后果为

final class RunnableTest$Lambda$1 implements Function {
    private final RunnableTest arg$1;
    private final int arg$2;

    private RunnableTest$Lambda$1(RunnableTest var1, int var2) {
        this.arg$1 = var1;
        this.arg$2 = var2;
    }

    private static Function get$Lambda(RunnableTest var0, int var1) {return new RunnableTest$Lambda$1(var0, var1);
    }

    @Hidden
    public Object apply(Object var1) {return this.arg$1.lambda$run$0(this.arg$2, (Integer)var1);
    }
}

创立完 inner class 之后,就是生成须要的 CallSite 了。如果没有参数,则生成这个 inner class 的一个 function interface 对象示例,创立一个固定返回这个对象的 MethodHandle,再包装成 ConstantCallSite 返回。
如果有参数,则返回一个须要每次调用 Factory 办法产生 function interface 的对象实例的 MethodHandle,包装成 ConstantCallSite 返回。

这样就实现了 bootstrap 的过程。invokedynamic 链接完之后,前面的调用就间接调用到对应的 MethodHandle 了,具体是实现就是返回固定的外部类对象,或每次创立新外部类对象。

再次比照通过 invokedynamic 绝对于间接匿名外部类语法糖的劣势

咱们再想一下,Java8 实现这一套骚操作的起因是什么。既然 lambda 表达式又不须要什么动静分派(调动哪个办法是明确的), 为什么要用 invokedynamic 呢?
JVM 虚拟机的一个根本保障就是低版本的 class 文件也是可能在高版本的 JVM 上运行的,并且 JVM 虚拟机通过版本升级,是在一直优化和晋升性能的。

间接转换成外部类实现,诚然简略,但编译后的二进制字节码(包含第三方 jar 包等)内容就固定了,实现固定为创立外部类对象 +invoke{virtual, static, special, interface}调用。
将来晋升性能只能靠晋升创立类对象、invoke 指令调用这几个中央的优化。换个相熟点的说法就是这里写死了。
如果通过 invokedynamic 呢,javac 编译后把足够的信息保留了下来,在 JVM 执行时可能动静决定如何实现 lambda,也就能一直优化 lambda 表达式的实现,并放弃兼容性,给将来留下了更多可能。

总结

本文是我学习 lambda 的一些总结,介绍了 lambda 表达式呈现的起因、实现办法以及不同实现思路上的比照。对 lambda 常识也只是略看了一些代码、材料,如有谬误或不明确的中央还请大家有情指出。


微信公众号【程序员黄小斜】作者是前蚂蚁金服 Java 工程师,专一分享 Java 技术干货和求职成长心得,不限于 BAT 面试,算法、计算机根底、数据库、分布式、spring 全家桶、微服务、高并发、JVM、Docker 容器,ELK、大数据等。关注后回复【book】支付精选 20 本 Java 面试必备精品电子书。

正文完
 0