JDK 25 新特性之紧凑的源文件(Compact Source Files)
背景
JDK 25 发布了,其中包含了一些新特性,具体的变化可以参考 JDK 25 Release Notes。其中一个变化是 JDK 25 支持 紧凑的源文件(Compact Source Files) 这一特性,简单来说就是在 java 文件里可以跳过 class 这一层直接定义字段/方法,示例代码如下 ⬇️
// 下方的 java 代码用到了 JDK 25 的新特性,请用对应版本的 javac 进行编译
private String s = "几个汉字";
int add(int a, int b) {
return a + b;
}
void main() {
IO.println("The result of 1 + 1 is: " + add(1, 1));
IO.println(s);
}
但这个特性是如何实现的呢?本文对此进行探讨。
要点
如果一个 java 文件中包含不在任何类中的字段/方法,那么 java 编译器在处理这样的 java 文件时,会认为这样的 java 文件隐式声明了一个类,这个类中包含了那些字段/方法。这样的 java 文件被称为 紧凑的源文件(compact source file)。
在 紧凑的源文件(compact source file) 中隐式声明的类 有以下特点 ⬇️
- 是一个
final class,它属于unnamed package(§7.4.2) -
entendjava.lang.Object, 没有implement任何接口 - 有一个默认构造函数(没有其他构造函数)
- 中含有 紧凑的源文件(
compact source file) 中所包含的字段/方法 - 中必须有
launchable的main方法,否则会编译失败
严谨的描述请参考 JEP 512: Compact Source Files and Instance Main Methods ⬇️ (直接相关的内容在下图绿色框的位置)
正文
用代码验证
请将以下代码保存为 Main.java ⬇️ (这段代码与本文开头的 背景 部分的代码相同,为了方便查看,我把代码复制到这里了)
// 下方的 java 代码用到了 JDK 25 的新特性,请用对应版本的 javac 进行编译
private String s = "几个汉字";
int add(int a, int b) {
return a + b;
}
void main() {
IO.println("The result of 1 + 1 is: " + add(1, 1));
IO.println(s);
}
用以下命令可以运行 Main.java 中的 main 方法。
java Main.java
运行结果如下
The result of 1 + 1 is: 2
几个汉字
但是这样看不到 class 文件。我们仍旧用传统的方式吧。可以用以下命令编译 Main.java,然后再运行 main 方法 ⬇️
javac Main.java
java Main
运行结果是一样的。这次生成了 Main.class 文件,这说明编译器的确生成了一个类(虽然 Main.java 里没有显式声明任何类)。我们可以用 javap -p Main 命令来查看 Main.class 的内容。结果如下 ⬇️
Compiled from "Main.java"
final class Main {
private java.lang.String s;
Main();
int add(int, int);
void main();
}
由此可见 Main 这个类里,有
- 字段 (因为
Main.java里有字段 ) - 默认构造函数
- 方法(因为
Main.java里有 方法) - 方法(因为
Main.java里有 方法)
我画了对应的类图 ⬇️
classDiagram
`java.lang.Object` <|-- Main
Main: private String s
Main: Main()
Main: int add(int, int)
Main: void main()
至于为何编译器会这样处理,我找到了相关的参考资料。
编译器的处理
JEP 512: Compact Source Files and Instance Main Methods 中的描述
JEP 512: Compact Source Files and Instance Main Methods 中有以下内容
我把红色框里的内容复制到下方 ⬇️
Henceforth, if the Java compiler encounters a source file with fields and methods that are not enclosed in a class declaration, it will consider the source file to implicitly declare a class whose members are the unenclosed fields and methods. Such a source file is called a compact source file.
The implicitly declared class of a compact source file
- Is a
finaltop-level class in the unnamed package;- Extends
java.lang.Objectand does not implement any interfaces;- Has a default constructor with no parameters, and no other constructors;
- Has, as its members, the fields and methods in the compact source file; and
- Must have a launchable
mainmethod; if it does not, a compile-time error is reported.
大意如下 ⬇️
如果一个 java 文件中包含不在任何类中的字段/方法,那么 java 编译器在处理这样的 java 文件时,会认为这样的 java 文件隐式声明了一个类,而这个类包含了对应的字段/方法。这样的 java 文件被称为 紧凑的源文件(compact source file)。
在 紧凑的源文件(compact source file) 中隐式声明的类 有以下特点 ⬇️
- 是一个
final class,它属于unnamed package -
entendjava.lang.Object, 没有implement任何接口 - 有一个默认构造函数(没有其他构造函数)
- 中含有 紧凑的源文件(
compact source file) 中所包含的字段/方法 - 中必须有
launchable的main方法,否则会编译失败
The Java® Language Specification 中的 8.1.8. Implicitly Declared Classes 小节 里的描述
The Java® Language Specification 中的 8.1.8. Implicitly Declared Classes 小节 有如下的描述 ⬇️ (相关的内容在下图的绿色框里)
我就不复制过来了。按我的理解,大意如下 ⬇️
隐式声明的类 有如下特性
- 是顶层类(§7.6)(也就是说 不是嵌套类)
- 的名字是合法的标识符(§3.8),这个标识符是宿主系统决定的(也就说
Main.java的代码所对应的隐式类未必就是Main,也可能用其他名称)- 不是
abstract的- 是
final的- 属于
unnamed package(§7.4.2),且 是package access- 直接继承自
Object(§8.1.4)- 不直接实现任何接口
- 中包含 紧凑的源文件(Compact Source Files) 中声明的所有成员(即,字段/方法/成员类/成员接口)。紧凑的源文件(Compact Source Files) 中不允许声明实例初始化块/静态初始化块/构造函数
- 中有隐式声明的默认构造函数
如果 中没有候选
main方法 (§12.1.4),那么会编译失败
和 kotlin 的对比
我们用 kotlin 也可以写出类似的代码 ⬇️ (请将代码保存为 Main.kt)
val s: String = "几个汉字"
fun add(a: Int, b: Int): Int {
return a + b
}
fun main(): Unit {
println("The result of 1 + 1 is: " + add(1, 1))
println(s)
}
用以下命令可以编译 Main.kt
kotlinc Main.kt
编译之后,会生成 MainKt.class 文件。用 javap -p MainKt 命令可以查看 MainKt.class 文件的内容。结果如下 ⬇️
Compiled from "Main.kt"
public final class MainKt {
private static final java.lang.String s;
public static final java.lang.String getS();
public static final int add(int, int);
public static final void main();
public static void main(java.lang.String[]);
static {};
}
kotlin 的处理可以整理如下
在 Main.kt 中 | 在 MainKt.class |
|---|---|
val s | MainKt 类中的静态字段 s 以及静态方法 getS() |
add(Int, Int): Int 方法 | MainKt 类中的静态方法 int add(int, int) |
main(): Unit 方法 | MainKt 类中的静态方法 void main() 以及静态方法 main(java.lang.String[]) |
对应的类图如下 ⬇️
和 Scala 对比
我们用 Scala 也可以写出类似的代码 ⬇️ (请将代码保存为 Main.Scala)
val s: String = "几个汉字"
def add(a: Int, b: Int): Int = {
return a + b
}
@main def main(): Unit = {
println("The result of 1 + 1 is: " + add(1, 1))
println(s)
}
用如下的命令可以编译 Main.Scala ⬇️(我电脑上的 scalac 的版本是 3.6.2)
scalac Main.scala
编译之后,会生成以下 class 文件(还会有其他文件生成)
main.classMain$package.classMain$package$.class
可以用 javap -v -p main 来查看 main.class 文件的详细内容(结果较长,这里略去)。借助 javap -v -p main 命令提供的结果,我把 main.class 的内容手动转化成了对应的 java 代码 ⬇️
// 以下代码是我根据 javap -v -p main 的结果手动转化出来的,
// 不保证结果绝对准确,仅供参考
// (class 文件中的一些属性在 java 代码中很难或无法直接体现,故略去)
public final class main {
public main() {
super();
}
public static void main(java.lang.String[] args) {
try {
Main$package$.MODULE$.main();
} catch (scala.util.CommandLineParser.ParseError error) {
scala.util.CommandLineParser$.MODULE$.showError(error);
}
}
}
对另外两个 class 文件也可以进行类似地处理。它们对应的 java 代码如下 ⬇️
// 以下代码是我根据 javap -v -p 'Main$package' 的结果手动转化出来的,
// 不保证结果绝对准确,仅供参考
// (class 文件中的一些属性在 java 代码中很难或无法直接体现,故略去)
public final class Main$package {
public static int add(int var0, int var1) {
return Main$package$.MODULE$.add(var0, var1);
}
public static void main() {
Main$package$.MODULE$.main();
}
public static java.lang.String s() {
return Main$package$.MODULE$.s();
}
}
// 以下代码是我根据 javap -v -p 'Main$package$' 的结果手动转化出来的,
// 不保证结果绝对准确,仅供参考
// (class 文件中的一些属性在 java 代码中很难或无法直接体现,故略去)
public final class Main$package$ implements java.io.Serializable {
private static final java.lang.String s;
public static final Main$package$ MODULE$;
private Main$package$() {
super();
}
static {} {
MODULE$ = new Main$package$();
s = "几个汉字";
}
private java.lang.Object writeReplace() {
return new scala.runtime.ModuleSerializationProxy(Main$package$.class);
}
public java.lang.String s() {
return Main$package$.s;
}
public int add(int a, int b) {
return a + b;
}
public void main() {
// 下面 4 行如果写在同 1 行里就太长了,所以我把它们的内容写在 4 行里
scala.Predef$.MODULE$.println(
new java.lang.StringBuilder(24).append("The result of 1 + 1 is: ").
append(add(1, 1)).toString()
);
scala.Predef$.MODULE$.println(this.s());
}
}
相关类图如下 ⬇️
其他问题
main 方法的写法
本文所用到的代码中的 main 方法,看起来和传统的 main 方法写法不同,这个问题可以参考以下链接 ⬇️
类图如何画
本文中的类图采用了两种画法,第一种方式是在编辑文档时,按照 "Mermaid 图表"->"类图" 的方式来生成类图。第二种方式是前往 Mermaid Live Editor 网站。
例如本文最后一张类图就是通过 Mermaid Live Editor 网站来绘制的。对应的代码如下
classDiagram
`java.lang.Object` <|-- main
class main {
+ main()
+ main(String[]) void$
}
`java.lang.Object` <|-- `Main$package`
class `Main$package` {
+ add(int, int) int$
+ main() void$
+ s() String$
}
`java.io.Serializable` <|.. `Main$package$`
<<interface>> `java.io.Serializable`
`java.lang.Object` <|-- `Main$package$`
class `Main$package$` {
- final String s$
+ final Main$package$ MODULE$$
- Main$package$()
- writeReplace() Object
+ s() String
+ add(int, int) int
+ main() void
}