[Java] JDK 25 新变化之紧凑的源文件(Compact Source Files)

189 阅读1分钟

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) 中隐式声明的类 CC 有以下特点 ⬇️

  • CC 是一个 final class,它属于 unnamed package(§7.4.2)
  • CC entend java.lang.ObjectCC 没有 implement 任何接口
  • CC 有一个默认构造函数(没有其他构造函数)
  • CC 中含有 紧凑的源文件(compact source file) 中所包含的字段/方法
  • CC 中必须有 launchable 的 main 方法,否则会编译失败

严谨的描述请参考 JEP 512: Compact Source Files and Instance Main Methods ⬇️ (直接相关的内容在下图绿色框的位置)

image.png

正文

用代码验证

请将以下代码保存为 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 这个类里,有

  • 字段 ss (因为 Main.java 里有字段 ss)
  • 默认构造函数
  • int add(int,int)int\ add(int, int) 方法(因为 Main.java 里有 int add(int,int)int\ add(int, int) 方法)
  • mainmain 方法(因为 Main.java 里有 mainmain 方法)

我画了对应的类图 ⬇️

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 中有以下内容

image.png

我把红色框里的内容复制到下方 ⬇️

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 final top-level class in the unnamed package;
  • Extends java.lang.Object and 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 main method; if it does not, a compile-time error is reported.

大意如下 ⬇️

如果一个 java 文件中包含不在任何类中的字段/方法,那么 java 编译器在处理这样的 java 文件时,会认为这样的 java 文件隐式声明了一个类,而这个类包含了对应的字段/方法。这样的 java 文件被称为 紧凑的源文件(compact source file)。 在 紧凑的源文件(compact source file) 中隐式声明的类 CC 有以下特点 ⬇️

  • CC 是一个 final class,它属于 unnamed package
  • CC entend java.lang.ObjectCC 没有 implement 任何接口
  • CC 有一个默认构造函数(没有其他构造函数)
  • CC 中含有 紧凑的源文件(compact source file) 中所包含的字段/方法
  • CC 中必须有 launchable 的 main 方法,否则会编译失败
The Java® Language Specification 中的 8.1.8. Implicitly Declared Classes 小节 里的描述

The Java® Language Specification 中的 8.1.8. Implicitly Declared Classes 小节 有如下的描述 ⬇️ (相关的内容在下图的绿色框里) image.png

我就不复制过来了。按我的理解,大意如下 ⬇️

隐式声明的类 CC 有如下特性

  • CC 是顶层类(§7.6)(也就是说 CC 不是嵌套类)
  • CC 的名字是合法的标识符(§3.8),这个标识符是宿主系统决定的(也就说 Main.java 的代码所对应的隐式类未必就是 Main,也可能用其他名称)
  • CC 不是 abstract
  • CCfinal
  • CC 属于 unnamed package(§7.4.2),且 CCpackage access
  • CC 直接继承自 Object (§8.1.4)
  • CC 不直接实现任何接口
  • CC 中包含 紧凑的源文件(Compact Source Files) 中声明的所有成员(即,字段/方法/成员类/成员接口)。紧凑的源文件(Compact Source Files) 中不允许声明实例初始化块/静态初始化块/构造函数
  • CC 中有隐式声明的默认构造函数

如果 CC 中没有候选 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.ktMainKt.class
val sMainKt 类中的静态字段 s 以及静态方法 getS()
add(Int, Int): Int 方法MainKt 类中的静态方法 int add(int, int)
main(): Unit 方法MainKt 类中的静态方法 void main() 以及静态方法 main(java.lang.String[])

对应的类图如下 ⬇️

mermaid-diagram-2025-09-24-185818.png

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.class        
  • Main$package.class
  • Main$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());
  }
}

相关类图如下 ⬇️

mermaid-diagram-2025-09-24-210357.png

其他问题

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
}

参考资料