前言

无。


Lambda

闭包函数,取代匿名内部类,用于实现判断、过滤、排序等功能:

Consumer<String> consumer = System.out::println;
consumer.accept("Hello World!");

List<String> list = new ArrayList<>() {{
    add("1");
    add("2");
    add("3");
}};
list.forEach(System.out::print);

此外,还有Predicate、Function、Supplier等多种功能接口。

MethodHandle

Java Lambda的实现与MethodHandle有关,这是一种类似反射中Method的东西,指向一个函数的具体实现,同样可以通过它进行反射函数调用:

Main main = new Main();
MethodType methodType = MethodType.methodType(void.class);
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
try {
    MethodHandle methodHandle = lookup.findVirtual(main.getClass(), "test", methodType);
    methodHandle.invoke(main);
}catch (Throwable e) {
    System.out.println(e.getMessage());
}

类字节码分析

整个测试用类如下:

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        MethodType methodType = MethodType.methodType(void.class);
        MethodHandles.Lookup lookup = MethodHandles.publicLookup();
        try {
            MethodHandle methodHandle = lookup.findVirtual(main.getClass(), "test", methodType);
            methodHandle.invoke(main);
        }catch (Throwable e) {
            System.out.println(e.getMessage());
        }
    }

    public void test() {
        Consumer<String> consumer = System.out::println;
        consumer.accept("Hello World!");

        List<Integer> list = new ArrayList<>() {{
            add(1);
            add(2);
            add(3);
        }};
        list.stream().filter(i -> i > 1).forEach(System.out::print);
    }
}

先看比较简单的Consumer接口调用,观察test函数第一部分的字节码:

 0 getstatic #48 <java/lang/System.out : Ljava/io/PrintStream;>
 3 dup
 4 invokestatic #64 <java/util/Objects.requireNonNull : (Ljava/lang/Object;)Ljava/lang/Object;>
 7 pop
 8 invokedynamic #70 <accept, BootstrapMethods #0>
13 astore_1
14 aload_1
15 ldc #74 <Hello World!>
17 invokeinterface #76 <java/util/function/Consumer.accept : (Ljava/lang/Object;)V> count 2

首先通过getstatic指令拿到了System.out.PrintStream这个println静态函数所属的类,然后dup复制再用invokestatic指令保证其不为空,接着是是与lambda强相关的invokedynamic指令。该指令指向的常量池#70包括一段常量池信息#71和一个Bootstrap方法#0:

常量池信息#71是一段NameAndType类型的信息,包含方法名和方法描述符,看起来分别是待调用的接口函数名accept,接口具体实现即println有关的类PrintStream,以及接口名Consumer,具体用处尚不清楚。

Bootstrap方法#0包含一个MethodHandle相关的常量池信息#144,和三个输入参数:

MethodHandle如上文所说,指向的是一个函数的具体实现,这里的两个MethodHandle中PrintStream.println是Consumer接口的具体实现,那么常量池信息#144指向的MethodHandle就是将这些杂乱数据合并的工具:

<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;>

该MethodHandle实际上是LambdaMetafactory类的metafactory函数,该函数有多个输入参数,这些参数的意义可以从定义和注释中一窥一二:

public static CallSite metafactory(MethodHandles.Lookup caller,
                                    String interfaceMethodName,
                                    MethodType factoryType,
                                    MethodType interfaceMethodType,
                                    MethodHandle implementation,
                                    MethodType dynamicMethodType)

从注释里可以看到这些函数的具体含义,caller看起来是虚拟机自动处理的参数,其他参数可以通过调试的方式看到具体数据,基本都是前面常量池中看到的数据:

该函数返回一个CallSite对象,继续调试,进入InnerClassLambdaMetafactory类的buildCallSite函数。该函数首先调用spinInnerClass函数,在generateInnerClass函数中创造了一个匿名内部类,最后将接口的具体实现写入到内存中:

// Forward the SAM method
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, interfaceMethodName,
                                    interfaceMethodType.toMethodDescriptorString(), null, null);
new ForwardingMethodGenerator(mv).generate(interfaceMethodType);

根据参考文章,可以使用运行配置将这些内存匿名类打印出来:

-Djdk.internal.lambda.dumpProxyClasses=<path>

结果如下:

package org.example;

import java.io.PrintStream;
import java.util.function.Consumer;

// $FF: synthetic class
final class Main$$Lambda$14 implements Consumer {
    private final PrintStream arg$1;

    private Main$$Lambda$14(PrintStream var1) {
        this.arg$1 = var1;
    }

    public void accept(Object var1) {
        this.arg$1.println((String)var1);
    }
}

实际上就是一个继承并实现了Consumer接口的匿名类,我们在代码中写的lambda表达式都会被写入到accept函数中,后面字节码在接口调用时就能触发。

最后实例化为一个ConstantCallSite对象返回,并将一个MethodHandle绑定到该CallSite里:

MethodHandle mh = caller.findConstructor(innerClass, constructorType);
return new ConstantCallSite(mh.asType(factoryType));

在经过一系列底层代码后,会调用MethodHandle的invokeBasic方法,得到对应的Consumer对象,并其将作为指令结果压入栈中。

在后续的指令中,会通过invokeinterface指令调用该对象的accept函数,从而调用到我们写的lambda表达式。

然后是第二部分,在filter中写的稍微复杂一点的lambda表达式,其主要的差别在于Bootstrap方法:

与前一个Bootstrap方法不同,前一个Bootstrap方法的MethodHandle直接使用的是PrintStream.println,而这一个Bootstrap方法使用的是一个匿名函数,该函数的字节码如下:

 0 aload_0
 1 invokevirtual #105 <java/lang/Integer.intValue : ()I>
 4 iconst_1
 5 if_icmple 12 (+7)
 8 iconst_1
 9 goto 13 (+4)
12 iconst_0
13 ireturn

实际上就是用我们写的lambda表达式生成的一个匿名函数,其他操作都差不多。


参考

Java Lambda表达式详解

同事:Lambda都不懂,你还写什么Java


Java

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

tai-e学习(2)
tai-e学习(1)