08 匿名内部类与 lambda —— lambda 表达式背后的字节码原理

问:lambda 表达式是匿名内部类的语法糖吗

答案在末尾

一、测试匿名内部类

public class Test {
    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello, inner class");
            }
        };
        r1.run();
    }
}

javap 查看字节码

javap -c -v Test

复制 main() 函数部分,如下

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #16                 // class Test$1
         3: dup
         4: invokespecial #18                 // Method Test$1."<init>":()V
         7: astore_1
         8: aload_1
         9: invokeinterface #19,  1           // InterfaceMethod java/lang/Runnable.run:()V
        14: return
  • 6 ~ 8 行:新建 Test$1 实例对象
  • 9 行:astore_1 指令把对象引用放到局部变量表下标为 1 的位置
  • 10 行:aload_1 指令将对象引用从局部变量表下标为 1 的位置放到操作数栈栈顶
  • 11 行:执行 Test$1 对象的 run 方法

这里的 Test$1 实例对象就是代码中的 r1

二、测试 lambda 表达式

还是上面的代码,改成 lambda 的形式

public class Test {
    public static void main(String[] args) {
        Runnable r1 = () -> {
            System.out.println("hello, lambda");
        };
        r1.run();
    }
}

javap 查看字节码

javap -c -v Test

复制 main() 函数和 lambda 相关的部分,如下

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #16,  0             // InvokeDynamic #0:run:()Ljava/lang/Runnable;
         5: astore_1
         6: aload_1
         7: invokeinterface #20,  1           // InterfaceMethod java/lang/Runnable.run:()V
        12: return

private static void lambda$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #29                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #35                 // String hello, lambda
         5: invokevirtual #37                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
  • 12 ~ 20 行:这是一个名为 lambda$0 的静态方法,方法内容很简单,打印输出字符串 hello, lambda
  • 6 行:invokedynamic 指令很神奇,#16 表示常量池的 #16

下面看下常量池

Constant pool:
    ...
    #16 = InvokeDynamic      #0:#17         // #0:run:()Ljava/lang/Runnable;
    #17 = NameAndType        #18:#19        // run:()Ljava/lang/Runnable;
    #18 = Utf8               run
    #19 = Utf8               ()Ljava/lang/Runnable;
    ...

常量池的 #16 所指向的内容中,#0 对应 BootstrapMethods 中的第 0 行

核心的 metafactory 定义

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

参数

  • caller:JVM 提供的查找上下文
  • invokedName:表示调用函数名,在本例中
  • invokedName 为 “run”
  • samMethodType:函数式接口定义的方法签名(参数类型和返回值类型)本例中为 run 方法的签名 “()void”
  • implMethod:编译时生成的 lambda 表达式对应的静态方法 invokestatic Test:lambda$main$0
  • instantiatedMethodType:一般和 samMethodType 是一样的(或是它的一个特例),在本例中是 “()void”

核心的 metafactory() 函数

public static CallSite metafactory(MethodHandles.Lookup caller,
                                   String invokedName,
                                   MethodType invokedType,
                                   MethodType samMethodType,
                                   MethodHandle implMethod,
                                   MethodType instantiatedMethodType) throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf;
    // 创建了 InnerClassLambdaMetafactory 类实例
    mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                         invokedName, samMethodType,
                                         implMethod, instantiatedMethodType,
                                         false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

查看 InnerClassLambdaMetafactory 类

public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
                                   MethodType invokedType,
                                   String samMethodName,
                                   MethodType samMethodType,
                                   MethodHandle implMethod,
                                   MethodType instantiatedMethodType,
                                   boolean isSerializable,
                                   Class<?>[] markerInterfaces,
                                   MethodType[] additionalBridges)
        throws LambdaConversionException {
    super(caller, invokedType, samMethodName, samMethodType,
          implMethod, instantiatedMethodType,
          isSerializable, markerInterfaces, additionalBridges);
    implMethodClassName = implDefiningClass.getName().replace('.', '/');
    implMethodName = implInfo.getName();
    implMethodDesc = implMethodType.toMethodDescriptorString();
    implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
            ? implDefiningClass
            : implMethodType.returnType();
    constructorType = invokedType.changeReturnType(Void.TYPE);
    // 关键:看这里,从名字就可以看出这是生成了 lambda 类的名字,生成规则是:类名 + $$Lambda$ + 1(每次获取值时先自动增 1)
    lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
    cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    int parameterCount = invokedType.parameterCount();
    if (parameterCount > 0) {
        argNames = new String[parameterCount];
        argDescs = new String[parameterCount];
        for (int i = 0; i < parameterCount; i++) {
            argNames[i] = "arg$" + (i + 1);
            argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
        }
    } else {
        argNames = argDescs = EMPTY_STRING_ARRAY;
    }
}

打印一下 lambda 的类名
打印一下 lambda 的类名
可以看到看到了生成的 lambda 类名为 Test$$Lambda$1/531885035

下面我们打个断点看看,那断点打在哪?
自然是上面说到的 InnerClassLambdaMetafactory 的构造方法中生成 lambdaClassName 的这行代码,代码执行到此行(下图的 165 行)时,再执行一步(下图的 166 行),然后鼠标停留在 lambdaClassName 上方,结果显示 Test$$Lambda$1
打个断点看看

控制台打印的是 Test$$Lambda$1/531885035,断点打印的是 Test$$Lambda$1,肯定在断点之后,类名加上了 /531885035

再次贴出 metafactory() 函数

public static CallSite metafactory(MethodHandles.Lookup caller,
                                   String invokedName,
                                   MethodType invokedType,
                                   MethodType samMethodType,
                                   MethodHandle implMethod,
                                   MethodType instantiatedMethodType) throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf;
    // 创建了 InnerClassLambdaMetafactory 类实例
    mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                         invokedName, samMethodType,
                                         implMethod, instantiatedMethodType,
                                         false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    // 这里对上面的 InnerClassLambdaMetafactory 实例调用了 buildCallSite() 方法
    // 因此生成的 Lambda 类名中斜杆及之后的数字,应该在此方法中生成
    return mf.buildCallSite();
}

查看 InnerClassLambdaMetafactory#buildCallSite() 方法

CallSite buildCallSite() throws LambdaConversionException {
    // 看下第一行
    final Class<?> innerClass = spinInnerClass();
    ...
}

看下 InnerClassLambdaMetafactory#spinInnerClass() 方法

private Class<?> spinInnerClass() throws LambdaConversionException {
    String[] interfaces;
    String samIntf = samBase.getName().replace('.', '/');
    boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);
    if (markerInterfaces.length == 0) {
        interfaces = new String[]{samIntf};
    } else {
        // Assure no duplicate interfaces (ClassFormatError)
        // 确保没有重复的接口
        Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
        itfs.add(samIntf);
        for (Class<?> markerInterface : markerInterfaces) {
            itfs.add(markerInterface.getName().replace('.', '/'));
            accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
        }
        interfaces = itfs.toArray(new String[itfs.size()]);
    }

    cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
             lambdaClassName, null,
             JAVA_LANG_OBJECT, interfaces);

    ...

    // Define the generated class in this VM.

    final byte[] classBytes = cw.toByteArray();

    // If requested, dump out to a file for debugging purposes
    // 如果被请求,为了调试程序,转储时生成一个文件
    if (dumper != null) {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            @Override
            public Void run() {
                dumper.dumpClass(lambdaClassName, classBytes);
                return null;
            }
        }, null,
        new FilePermission("<<ALL FILES>>", "read, write"),
        // createDirectories may need it
        new PropertyPermission("user.dir", "read"));
    }

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

1 ~ 21 行,大致就是为了生成不重复的接口,所以在后面加上了 /531885035

说了半天,到底生成的这个 lambda 的类在哪能看到
仔细看看上面的 InnerClassLambdaMetafactory#spinInnerClass() 方法,其中 31 ~42 行可以用来控制是否将生成的类 dump 到磁盘中
那如何进入这个 if 语句,去看看 dumper 变量

// For dumping generated classes to disk, for debugging purposes
// 为了调试程序,将转储的类文件生成到磁盘
private static final ProxyClassesDumper dumper;

static {
    final String key = "jdk.internal.lambda.dumpProxyClasses";
    String path = AccessController.doPrivileged(
            new GetPropertyAction(key), null,
            new PropertyPermission(key , "read"));
    dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
}

到这里,肯定是在执行 java 命令时传递一些什么参数,就能使 dumper 值为 true

java 命令生成 dump 类文件

// 注意:. Test 之间有个空格
// . 表示当前目录
// 将 dump 的类文件生成到在当前目录
java -Djdk.internal.lambda.dumpProxyClasses=. Test

java 命令生成 dump 类文件
可以看到生成了一个内部类:Test$$Lambda$1.class,这个类是由 InnerClassLambdaMetafactory 使用 ASM 字节码技术动态生成的,默认情况下看不到而已

我们 javap 一下这个内部类 Test$$Lambda$1.class 吧

javap -c -v Test$$Lambda$1

全部内容,如下

final class Test$$Lambda$1 implements java.lang.Runnable
  minor version: 0
  major version: 52
  flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
Constant pool:
   #1 = Utf8               Test$$Lambda$1
   #2 = Class              #1             // Test$$Lambda$1
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               java/lang/Runnable
   #6 = Class              #5             // java/lang/Runnable
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = NameAndType        #7:#8          // "<init>":()V
  #10 = Methodref          #4.#9          // java/lang/Object."<init>":()V
  #11 = Utf8               run
  #12 = Utf8               Ljava/lang/invoke/LambdaForm$Hidden;
  #13 = Utf8               Test
  #14 = Class              #13            // Test
  #15 = Utf8               lambda$0
  #16 = NameAndType        #15:#8         // lambda$0:()V
  #17 = Methodref          #14.#16        // Test.lambda$0:()V
  #18 = Utf8               Code
  #19 = Utf8               RuntimeVisibleAnnotations
{
  public void run();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #17                 // Method Test.lambda$0:()V
         3: return
    RuntimeVisibleAnnotations:
      0: #12()
}
  • 1 行:这个类实现了 Runnable 接口
  • 31 行:在 run() 方法 中,invokestatic 指令调用了 Method Test.lambda$0:()V,即 Test 的静态方法 lambda$0()

至此,简述一下整个过程

  • lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时编译器会生成一个对应的引导方法的统计(原文说是生成一个对应的引导方法 Bootstrap Method,根据 javap 打印的结果,应该是一个 BootstrapMethods,如果这里直接翻译成引导方法,会误以为是一个起引导作用的方法,而实际生成的信息看上去是一个和常量池一样形式的内容,只是记录了一些方法信息而已),然后会在第一次执行 invokedynamic 指令时调用 LambdaMetafactory#metafactory() 这个引导方法动态生成内部类 Test$$Lambda$1.class,默认这个类不会生成文件,所以看不到
  • 引导方法 LambdaMetafactory#metafactory() 会返回一个 CallSite 对象,这个 CallSite 会链接上面生成的 Test$$Lambda$1.class 内部类(实现了 Runnable 接口)
  • lambda 表达式中的内容会被编译成静态方法,前面动态生成的内部类会直接调用该静态方法
  • 真正执行 lambda 调用的还是用 invokeinterface 指令

画个图
lambda方法编译到执行流程

小结

  • lambda 表达式在编译期只是生成了一个 invokedynamic 指令,并没有生成匿名内部类
  • lambda 表达式中的内容会被编译成一个静态方法
    到时候调用这个静态方法,就相当于执行了 lambda 表达式,那么谁来调用这个静态方法呢
    既然编译期并没有生成别的什么,只有一个静态方法,那么自然是在运行期做一些小动作了
    这个小动作就是,在运行期 LambdaMetafactory.metafactory() 这个工厂方法会根据一些参数生成一个内部类,这个内部类会去调用编译期生成的那个静态方法
  • 可以看到,其实整个过程还是会生成一个内部类,只不过 lambda 表达式对应的这个内部类是在运行期生成的

思考题

下面的两段代码分别会生成多少个内部类?为什么?

代码片段 1

for (int i = 0; i < 10; i++) {
    Runnable r = () -> {
        System.out.println("hello, lambda");
    };
    r.run();
}

代码片段 2

Runnable r1 = () -> {
    System.out.println("hello, lambda");
};
r1.run();

Runnable r2 = () -> {
    System.out.println("hello, lambda");
};
r2.run();

回答

思考题答案:代码片段 1 生成一个内部类,代码片段 2 生成两个内部类,因为只有第一次执行 invokedynamic 指令时才会生成对应的内部类

开头问题答案:lambda 表达式不仅仅是想象中的匿名内部类的语法糖那么简单,背后的整个运行逻辑和单纯的匿名内部类是不一样的

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
lambda表达式是Java 8引入的一种新特性,用于简化代码编写和提高代码的可读性。它可以用来替代匿名类的写法,并且可以在各种上下文中使用。 一个常见的用法是用lambda表达式实现函数式接口的方法。比如,可以使用lambda表达式来实现Runnable接口的run方法,或者实现ActionListener接口的actionPerformed方法。 另外,lambda表达式也可以用来对列表进行迭代。通过使用forEach方法,可以很方便地对列表中的元素进行操作。 与匿名类相比,lambda表达式有一些不同之处。一个关键的区别是关键字this的指向。在匿名类中,this关键字指向匿名类本身,而在lambda表达式中,this关键字指向包围lambda表达式的类。此外,lambda表达式的编译方式也不同于匿名类。Java编译器将lambda表达式编译成类的私有方法,并使用invokedynamic字节码指令来动态绑定这个方法。 因此,lambda表达式是一种更简洁、更灵活的编码方式,可以帮助我们更好地实现函数式编程的思想。它在Java编程中的应用非常广泛。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Java lambda表达式10个示例](https://blog.csdn.net/Byd_chao/article/details/123534057)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值