JDK 25 新特性之简化的 main 方法
背景
JDK 25 发布了,其中包含了一些新特性,具体的变化可以参考 JDK 25 Release Notes。其中有一个变化是 main 方法可以用更简洁的方式来写了 ⬇️
void main() {
IO.println("Hello, World!");
}
但这个特性是如何实现的呢?本文对此进行探讨。
要点
1. 对 main 方法的限制的变化
对作为应用入口的 main 方法的要求 ⬇️
| JDK 版本 | 允许的 access | 返回值 | 入参 | 是否 static |
|---|---|---|---|---|
JDK 24 | 必须是 public | void | 入参是 String 的数组(String[]或者String... 都可以) | 必须是 static |
JDK 25 | public/protected/package access 都可以 | void | 没有入参 或者入参是 String 的数组(String[]或者String... 都可以) | 是或 否 都可以 |
有变化的地方,我用粗体标出来了 ⬆️
2. java.lang.IO 类
JDK 25 新增了 java.lang.IO 类,其中有静态方法 println(Object)
正文
JEP 512: Compact Source Files and Instance Main Methods 对 main 方法的变化有详细的介绍,读者朋友如果有兴趣的话,可以看一看。
我们写点代码来看看到底发生了什么 ⬇️ (请将以下代码保存为 SimpleMain.java,其实具体的名称并不重要,不过为了方便下文的描述,我们还是用一个统一的名字)
// This is a complete and runnable program!
void main() {
IO.println("Hello, World!");
}
用如下的命令可以编译 SimpleMain.java ⬇️
javac SimpleMain.java
编译之后,会生成 SimpleMain.class 文件。用以下命令可以查看 SimpleMain.class 的内容
javap -v -p SimpleMain
生成的结果如下 ⬇️ (最开头的 4 行已略去)
final class SimpleMain
minor version: 0
major version: 69
flags: (0x0030) ACC_FINAL, ACC_SUPER
this_class: #15 // SimpleMain
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = String #8 // Hello, World!
#8 = Utf8 Hello, World!
#9 = Methodref #10.#11 // java/lang/IO.println:(Ljava/lang/Object;)V
#10 = Class #12 // java/lang/IO
#11 = NameAndType #13:#14 // println:(Ljava/lang/Object;)V
#12 = Utf8 java/lang/IO
#13 = Utf8 println
#14 = Utf8 (Ljava/lang/Object;)V
#15 = Class #16 // SimpleMain
#16 = Utf8 SimpleMain
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 main
#20 = Utf8 SourceFile
#21 = Utf8 SimpleMain.java
{
SimpleMain();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
void main();
descriptor: ()V
flags: (0x0000)
Code:
stack=1, locals=1, args_size=1
0: ldc #7 // String Hello, World!
2: invokestatic #9 // Method java/lang/IO.println:(Ljava/lang/Object;)V
5: return
LineNumberTable:
line 3: 0
line 4: 5
}
SourceFile: "SimpleMain.java"
由于结果并不复杂,我们可以手动写出对应的 java 代码 ⬇️
// 以下代码是我手动转化的,不保证绝对准确,仅供参考
final class SimpleMain {
SimpleMain() {
}
void main() {
IO.println("Hello, World!");
}
}
从 class 文件以及手动转化出的 java 文件可以看到
main方法的变化- 这里的
main方法 不是public static final的 - 这里的
main方法 没有String[]类型的入参
- 这里的
main方法里调用了java.lang.IO的println(...)方法。
我们来看看这是如何做到的。
1. main 方法的变化
1.1 调用 main 方法
以前我们写的都是以下 2 种 main 方法
public static void main(String[] args)或者public static void main(String... args)
为何现在可以打破这些限制呢?
简单来说,就是 Java Language Specification 做了相关的调整。
在 JDK 24 的 相关描述 中,可以看到 ⬇️
所以截止到 JDK 24 时,作为程序入口的 main 方法还必须是以下两种形式之一
public static void main(String[] args)或者public static void main(String... args)
而在 JDK 25 中,同样的 章节 里,描述调整成这样了 ⬇️
这一小节的完整内容有点长,我把绿色框里的内容复制到下方
A
mainmethod declared in, or inherited by, a given class is a candidatemainmethod if either:
- It has a single formal parameter (§8.4.1) whose declared type is an array of
String, avoidresult, andpublic,protectedor package access; or- It has no formal parameters, a
voidresult, andpublic,protectedor package access.
简单点说,就是原先那样的 public static void main(String[])/public static void main(String...) 方法继续有效,但现在对 main 方法的限制更宽松了(精确的描述请参考原文)⬇️
允许的 access | 返回值 | 入参 | 是否 static |
|---|---|---|---|
public/protected/package access 都可以 | void | 没有入参 或者入参是 String 的数组(String[]或者String... 都可以) | 是或 否 都可以 |
1.2 main 方法在哪个类里?
虽然对 main 方法的限制变宽松了,但它还是存在于一个类中,这个类(即 SimpleMain)是怎么出现的呢?
JEP 512: Compact Source Files and Instance Main Methods 里提到
大意是说,如果 java 编译器遇到了不在任何类之内的字段/方法,则编译器会认为有隐式声明的类,来包含这样的字段/方法。
所以 SimpleMain 这个类是编译器检测到 void main() 方法后自动生成的类。
按照 The Java® Language Specification 中的 12.1.4. Invoke a main Method 小节 的描述,如果 main 方法是实例方法的话,在调用这个 main 方法前,会先创建对应类的实例 ⬇️
我们可以写点代码验证一下 ⬇️
非静态 main 方法需要在实例创建后才会被调用
请将以下代码保存为 SimpleMain2.java ⬇️
public class SimpleMain2 {
public SimpleMain2() {
IO.println("Hi, the constructor of SimpleMain2 is called");
}
void main(String[] arg) {
IO.println("Hello, World!");
}
}
以下命令可以编译 SimpleMain2.java 并运行其中的 main 方法。
javac SimpleMain2.java
java SimpleMain2
运行结果如下
Hi, the constructor of SimpleMain2 is called
Hello, World!
从运行结果中可以看到,的确是先创建了 SimpleMain2 的实例,然后再调用对应的 main 方法。
2. java.lang.IO 的 println(...) 方法
JDK 25 的 java.lang package 中增加了
IO.java,而其中有静态方法 println(Object) ⬇️ (其实它就是直接调用了 System.out.println(Object) 方法)
/**
* Writes a string representation of the specified object and then writes
* a line separator to the standard output.
*
* <p> The effect is as if {@link java.io.PrintStream#println(Object) println(obj)}
* had been called on {@code System.out}.
*
* @param obj the object to print, may be {@code null}
*/
public static void println(Object obj) {
System.out.println(obj);
}
根据 The Java® Language Specification 中的 Chapter 7. Packages and Modules 中的描述(在下方)可知,java.lang 这个 package 里所有 public 的 class 和 interface 都会被自动 import。java.lang.IO 这个新增的 class 是 public 的,所以我们不必显式写出 import java.lang.IO; 这样的语句。
Code in a compilation unit automatically has access to all classes and interfaces declared in its package and also automatically imports all of the
publicclasses and interfaces declared in the predefined packagejava.lang.