[Java] 如何将 Lambda 表达式对应的类保存到 class 文件中?

背景

大家在日常开发中应该都用过 <math xmlns="http://www.w3.org/1998/Math/MathML"> Lambda \text{Lambda} </math>Lambda 表达式。以下面的 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 代码为例,我们在运行 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld \text{HelloWorld} </math>HelloWorld 中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> main \text{main} </math>main 方法时,可以看到 <math xmlns="http://www.w3.org/1998/Math/MathML"> r \text{r} </math>r 的类型信息,但是在编译时、运行时都没有看到 <math xmlns="http://www.w3.org/1998/Math/MathML"> r \text{r} </math>r 对应的类的 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件,有没有办法将 <math xmlns="http://www.w3.org/1998/Math/MathML"> r \text{r} </math>r 对应的类保存到 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件里呢?本文会对此进行探讨。

java 复制代码
public class HelloWorld {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("Hello World");
        r.run();
        System.out.println("class name of r: " + r.getClass().getName());	
    }
}

要点

运行 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 命令时,使用以下两个选项中的任何一个,都可以把 <math xmlns="http://www.w3.org/1998/Math/MathML"> Lambda \text{Lambda} </math>Lambda 表达式对应的类自动保存到 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件中

  • -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
  • -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true

正文

<math xmlns="http://www.w3.org/1998/Math/MathML"> javac \text{javac} </math>javac 版本和 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 版本

查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> javac \text{javac} </math>javac 版本

使用如下命令可以查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> javac \text{javac} </math>javac 命令的版本

bash 复制代码
javac -version

该命令在我电脑上的运行结果如下

text 复制代码
javac 21

查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 版本

使用如下命令可以查看 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 命令的版本

bash 复制代码
java -version

该命令在我电脑上的运行结果如下

text 复制代码
openjdk version "21" 2023-09-19
OpenJDK Runtime Environment (build 21+35-2513)
OpenJDK 64-Bit Server VM (build 21+35-2513, mixed mode, sharing)

代码

请将如下代码保存为 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld.java \text{HelloWorld.java} </math>HelloWorld.java ⬇️

java 复制代码
public class HelloWorld {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("Hello World");
        r.run();
        System.out.println("class name of r: " + r.getClass().getName());	
    }
}

编译

使用如下的命令可以编译 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld.java \text{HelloWorld.java} </math>HelloWorld.java

bash 复制代码
javac HelloWorld.java

编译之后,执行 tree . 命令,会看到这样的结果 ⬇️ (当前目录多了 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld.class \text{HelloWorld.class} </math>HelloWorld.class 文件)

text 复制代码
.
├── HelloWorld.class
└── HelloWorld.java

1 directory, 2 files

运行

使用如下的命令可以编译 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld \text{HelloWorld} </math>HelloWorld 中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> main \text{main} </math>main 方法 ⬇️

bash 复制代码
java HelloWorld

在我电脑上,该命令的运行结果如下 ⬇️ (在您的电脑上,最后一行输出的类名可能是其他内容)

text 复制代码
Hello World
class name of r: HelloWorld$$Lambda/0x0000007001000a00

看来 <math xmlns="http://www.w3.org/1998/Math/MathML"> r \text{r} </math>r 的精确类型(为了便于描述,我们把它简称为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L \text{L} </math>L 吧)是一个很特殊的类(至少命名很特殊😂)。在运行完 <math xmlns="http://www.w3.org/1998/Math/MathML"> HelloWorld \text{HelloWorld} </math>HelloWorld 中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> main \text{main} </math>main 方法之后,当前目录并没有新的文件生成。如果我们想看看 <math xmlns="http://www.w3.org/1998/Math/MathML"> L \text{L} </math>L 的内容,一个思路是让 <math xmlns="http://www.w3.org/1998/Math/MathML"> java \text{java} </math>java 虚拟机把 <math xmlns="http://www.w3.org/1998/Math/MathML"> L \text{L} </math>L 对应的 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件生成出来。

InnerClassLambdaMetafactory.java 里可以找到如下的选项 ⬇️

看起来只要加上以下两个选项的任何一个,就可以将 <math xmlns="http://www.w3.org/1998/Math/MathML"> r \text{r} </math>r 对应的类保存到 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件里

  • -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
  • -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true

我们就用第一种方法吧,完整的命令如下 ⬇️

bash 复制代码
java -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles HelloWorld

运行完这个命令后,再运行 tree . 命令,会得到以下结果 ⬇️ (在您的电脑上,所看到的 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件的名称可能会有差异)

text 复制代码
.
├── DUMP_LAMBDA_PROXY_CLASS_FILES
│   └── HelloWorld$$Lambda.0x0000007001000a00.class
├── HelloWorld.class
└── HelloWorld.java

2 directories, 3 files

当前目录多了一个名为 DUMP_LAMBDA_PROXY_CLASS_FILES 的目录,这个目录下有一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件。我们用如下的命令可以查看这个 <math xmlns="http://www.w3.org/1998/Math/MathML"> class \text{class} </math>class 文件的内容

bash 复制代码
javap -v -p DUMP_LAMBDA_PROXY_CLASS_FILES/*class

我电脑上的运行结果如下 ⬇️ (这里略去了结果中的前三行)

text 复制代码
final class HelloWorld$$Lambda implements java.lang.Runnable
  minor version: 0
  major version: 65
  flags: (0x1030) ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
  this_class: #2                          // HelloWorld$$Lambda
  super_class: #4                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 2, attributes: 0
Constant pool:
   #1 = Utf8               HelloWorld$$Lambda
   #2 = Class              #1             // HelloWorld$$Lambda
   #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               HelloWorld
  #13 = Class              #12            // HelloWorld
  #14 = Utf8               lambda$main$0
  #15 = NameAndType        #14:#8         // lambda$main$0:()V
  #16 = Methodref          #13.#15        // HelloWorld.lambda$main$0:()V
  #17 = Utf8               Code
{
  private HelloWorld$$Lambda();
    descriptor: ()V
    flags: (0x0002) ACC_PRIVATE
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
         4: return

  public void run();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: invokestatic  #16                 // Method HelloWorld.lambda$main$0:()V
         3: return
}

由于它的内容并不多,我们可以尝试手动反编译它,反编译的结果如下 ⬇️

java 复制代码
final class HelloWorld$$Lambda implements java.lang.Runnable {
  private HelloWorld$$Lambda() {
    super();
  }

  public void run() {
      HelloWorld.lambda$main$0();
  }
}

简要的类图如下 ⬇️

其他

绘制"简要的类图"所用到的代码

我用了 IntelliJ IDEA 中的 PlantUML 插件来画那张图,完整的代码如下 ⬇️

text 复制代码
@startuml
'https://plantuml.com/component-diagram

title 简要的类图
caption \n\n
' caption 的内容是为了防止掘金平台生成的水印遮盖图中的文字

interface java.lang.Runnable
class HelloWorld$$Lambda
java.lang.Runnable <|.. HelloWorld$$Lambda

interface java.lang.Runnable {
    void run()
}

class HelloWorld$$Lambda {
  - HelloWorld$$Lambda()
  + void run()
}

note left of HelloWorld$$Lambda::run
<code>
public void run() {
    HelloWorld.lambda$main$0();
}
</code>
end note

@enduml
相关推荐
五月君_1 小时前
Bun v1.3.14 发布,Rust 版即将进 Claude Code 内测,下一版可能就告别 Zig
开发语言·后端·rust
明月_清风1 小时前
🍃 MongoDB 从入门到上手:一篇写给新手的科普指南
后端·mongodb
それども2 小时前
Gradle 构建疑难杂症 Could not find netty-transport-native-epoll-linux-aarch_64.ja
java·服务器·gradle·maven
正儿八经的少年2 小时前
application.yml 系列配置文件作用与区别
java·配置文件
鱼很腾apoc2 小时前
【学习篇】第20期 超详解 C++ 多态:从语法规则到底层原理
java·c语言·开发语言·c++·学习·算法·青少年编程
cheems95273 小时前
[Spring MVC] 统一功能与拦截器实践总结
java·spring·mvc
程序员cxuan3 小时前
当 00 后开始用 token 给学校送礼
人工智能·后端·程序员
Full Stack Developme4 小时前
Spring Boot 事务管理完整教程
java·数据库·spring boot
夕颜1114 小时前
opencli 使用总结
后端