JEP 512: 紧凑的源文件和实例 main 方法
JEP 512: 紧凑的源文件和实例 main 方法
作者:Ron Pressler, Jim Laskey, & Gavin Bierman
所有者:Gavin Bierman
类型:功能
范围:SE
状态:已关闭 / 已交付
发布版本:25
组件:specification / language
讨论:amber dash dev at openjdk dot org
相关内容:
- JEP 511: 模块导入声明 (Module Import Declarations)
- JEP 495: 简单源文件和实例
main方法 (第四次预览)
审阅人:Alex Buckley, Brian Goetz
支持者:Brian Goetz
创建时间:2024/11/21 11:58
更新时间:2025/07/11 06:45
问题编号:8344699
摘要
该 JEP 旨在演进 Java 编程语言,使初学者在编写他们的第一个程序时,无需理解为大型程序而设计的语言特性。这并非使用一种独立的语言方言,而是让初学者可以为单类程序编写精简的声明,并随着技能的增长,无缝地将程序扩展以使用更高级的特性。同样,有经验的开发者也可以简洁地编写小型程序,而无需使用那些为大型编程设计的构造。
历史
该功能最初由 JEP 445(JDK 21)提议作为预览版,随后由 JEP 463(JDK 22)、JEP 477(JDK 23)和 JEP 495(JDK 24)进行了改进和完善。我们在此提议在 JDK 25 中将该功能定稿,并将简单源文件更名为紧凑源文件,同时根据经验和反馈进行了一些小的改进:
- 用于基本控制台 I/O 的新
IO类现在位于java.lang包中,而非java.io包。因此,它被所有源文件隐式导入。 IO类的静态方法不再被隐式导入到紧凑源文件中。因此,除非这些方法被显式导入,否则对它们的调用必须指定类名,例如IO.println("Hello, world!")。IO类的实现现在基于System.out和System.in,而非java.io.Console类。
目标
- 为 Java 编程提供平滑的入门坡道,使教师能够以循序渐进的方式介绍概念。
- 帮助学生以简洁的方式编写简单的程序,并随着技能的增长优雅地扩展他们的代码。
- 减少编写其他类型的小型程序(如脚本和命令行工具)的繁琐步骤。
- 不引入独立的 Java 语言方言。
- 不引入独立的工具链。小型 Java 程序应与大型程序使用相同的工具进行编译和运行。
动机
Java 编程语言非常擅长处理由大型团队多年开发和维护的大型复杂应用程序。它拥有丰富的功能,用于数据隐藏、代码复用、访问控制、命名空间管理和模块化,这些功能允许组件被清晰地组合在一起,同时可以独立开发和维护。通过这些特性,组件可以暴露定义良好的接口与其他组件交互,同时隐藏内部实现细节,从而允许每个组件独立演进。事实上,面向对象范式从根本上说就是将通过定义良好的协议进行交互的组件组合在一起,同时抽象化实现细节。这种大型组件的组合被称为大型编程 (programming in the large)。
然而,Java 编程语言也旨在成为第一门编程语言。当程序员刚入门时,他们不会在团队中编写大型程序,而是独自编写小型程序。他们不需要封装和命名空间这些对不同人员编写的组件进行独立演进很有用的概念。在教授编程时,教师从小型编程 (programming in the small) 的基本概念开始,例如变量、控制流和子程序。在这个阶段,并不需要类、包和模块等大型编程概念。让这门语言对新手更友好符合 Java 资深用户的利益,但他们也可能更喜欢简洁地编写小型程序,而无需任何大型编程的构造。
考虑经典的 Hello, World! 示例,这通常是初学者的第一个程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
对于这个程序所做的事情来说,代码太多、概念太多、构造太多。
class声明和强制性的public访问修饰符都是大型编程的构造。当代码需要与外部组件通过定义良好的接口进行封装时,它们很有用,但在这种小例子中毫无意义。String[] args参数也存在于将代码与外部组件(在本例中是操作系统的 shell)连接。在这里它既神秘又无用,尤其是在像HelloWorld这样的小程序中它根本没有被使用。static修饰符是语言的类和对象模型的一部分。对于初学者来说,static不仅神秘,而且有害:为了向这个程序添加更多方法或字段,初学者要么必须将它们全部声明为static—— 这会传播一种既不常见也不是好习惯的惯用法 —— 要么必须面对静态和实例成员的区别,并学习如何实例化一个对象。- 初学者可能会被神秘的
System.out.println咒语进一步困惑,并想知道为什么一个简单的函数调用不能解决问题。即使在第一周的程序中,初学者也可能被迫学习如何为基本功能导入基本的工具类,并想知道为什么这些不能自动提供。
新程序员在最不恰当的时候遇到了这些概念,在他们学习变量和控制流之前,在他们无法理解大型编程构造对于保持大型程序组织良好的用途之前。教师通常会告诫:“别担心那些,你以后会明白的。”这让老师和学生都感到不满意,并给学生留下了这门语言很复杂的持久印象。
这项工作的动机不仅仅是减少繁琐的步骤。我们的目标是帮助对 Java 语言或编程总体而言的新手以正确的顺序学习这门语言:从基本的小型编程概念开始,例如进行简单的文本 I/O 和使用 for 循环处理数组,只有当高级的大型编程概念真正有益并且可以更容易被掌握时,再继续学习它们。
此外,这项工作的动机不仅仅是帮助初学者。我们的目标是帮助所有编写小型程序的人,无论是学生、编写命令行工具的系统管理员,还是为最终将在企业级软件系统中使用的核心算法进行原型设计的领域专家。
我们建议通过隐藏这些细节直到它们有用为止,来简化小型程序的编写,而不是改变 Java 语言的结构 —— 代码仍然封装在方法中,方法封装在类中,类封装在包中,包封装在模块中。
描述
首先,我们允许 main 方法省略臭名昭著的 public static void main(String[] args) 样板代码,这将 Hello, World! 程序简化为:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
其次,我们引入了一种紧凑形式的源文件,让开发者可以直接进入代码,而无需多余的类声明:
void main() {
System.out.println("Hello, World!");
}
第三,我们在 java.lang 包中添加了一个新类,为初学者提供基本的面向行的 I/O 方法,从而用一个更简单的形式取代了神秘的 System.out.println:
void main() {
IO.println("Hello, World!");
}
最后,对于那些超越 Hello, World! 并需要基本数据结构或文件 I/O 的程序,在紧凑源文件中,我们自动导入了除了 java.lang 包之外的一系列标准 API。
这些改变结合在一起,提供了一个入门坡道,一个平缓的斜坡,可以优雅地并入高速公路。当初学者进入大型程序时,他们不必丢弃早期学到的东西,而是会看到这一切如何融入更大的图景中。当经验丰富的开发者从原型转向生产时,他们可以平滑地将代码发展为大型程序中的组件。
实例 main 方法
为了编写和运行程序,初学者将学习程序的入口点。当前的 Java 语言规范(JLS)解释说,Java 程序的入口点是一个名为 main 的方法(§12.1):
Java 虚拟机通过调用某个指定类或接口的 main 方法来开始执行,并向其传递一个字符串数组作为单一参数。
JLS 进一步规定(§12.1.4):
main 方法必须声明为 public、static 和 void。它必须指定一个声明类型为 String 数组的形参。
对 main 声明的这些要求是历史性的且不必要的。我们可以通过两种方式简化 Java 程序的入口点:允许 main 非静态,并取消对 public 和数组参数的要求。这些改变允许我们编写 Hello, World! 时没有 public 修饰符,没有 static 修饰符,没有 String[] 参数,从而将这些构造的引入推迟到需要时:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
假设这个程序在 HelloWorld.java 文件中,我们可以直接使用源代码启动器运行它:$ java HelloWorld.java
或者,我们可以显式编译它,然后运行它:$ javac HelloWorld.java$ java HelloWorld
无论哪种方式,启动器都会启动 Java 虚拟机,然后选择并调用指定类的 main 方法:
- 如果类声明或继承了一个带有
String[]参数的main方法,则启动器会选择该方法。 - 否则,如果类声明或继承了一个没有参数的
main方法,则启动器会选择该方法。 - 否则,启动器会报告错误并终止。
- 如果所选方法是
static的,则启动器会调用它。 - 否则,所选方法是一个实例
main方法。该类必须有一个非私有的、无参数的构造函数。启动器会调用该构造函数,然后调用生成对象的所选main方法。如果没有这样的构造函数,则启动器会报告错误并终止。
根据此协议可以被选择和调用的任何 main 方法都称为可启动方法。例如,HelloWorld 类有一个可启动的 main 方法,即 void main()。
紧凑源文件
在 Java 语言中,每个类都位于一个包中,每个包都位于一个模块中。模块和包为类提供了命名空间和封装,但仅包含几个类的小型程序不需要这些概念。因此,开发者可以省略包和模块声明,他们的类将位于一个无名包的无名模块中。
类为字段和方法提供了命名空间和封装,但仅包含几个字段和方法的小型程序不需要这些概念。我们不应该要求初学者在熟悉变量、控制流和子程序等基本构建块之前就理解这些概念。因此,我们可以像不要求包或模块声明一样,停止要求小型程序包含类声明。
此后,如果 Java 编译器遇到一个源文件,其中包含未封装在类声明中的字段和方法,它将认为该源文件隐式声明了一个类,其成员就是那些未封装的字段和方法。这样的源文件被称为紧凑源文件。
有了这个改变,我们可以将 Hello, World! 编写为一个紧凑源文件:
void main() {
System.out.println("Hello, World!");
}
紧凑源文件的隐式声明类
- 是无名包中的一个
final顶层类; - 扩展了
java.lang.Object,并且不实现任何接口; - 有一个无参数的默认构造函数,没有其他构造函数;
- 其成员是紧凑源文件中的字段和方法;以及
- 必须有一个可启动的
main方法;如果没有,则报告编译时错误。
由于在紧凑源文件中声明的字段和方法被解释为隐式声明的类的成员,我们可以通过调用附近声明的方法来编写 Hello, World!:
String greeting() { return "Hello, World!"; }
void main() {
System.out.println(greeting());
}
或者通过访问一个字段:
String greeting = "Hello, World!";
void main() {
System.out.println(greeting);
}
紧凑源文件隐式声明了一个类,因此该类没有可以在源代码中使用的名称。Java 编译器在编译紧凑源文件时会生成一个类名,但该名称是实现特定的,不应在任何源代码中依赖 —— 甚至不应在紧凑源文件本身的源代码中。
我们可以通过 this 来引用该类的当前实例,无论是显式还是像上面那样隐式,但我们不能使用 new 操作符实例化该类。这反映了一个重要的权衡:如果初学者尚未学习类等面向对象概念,那么在紧凑源文件中编写代码就不应要求类声明 —— 因为正是类声明才能提供一个可与 new 一起使用的类名。
紧凑源文件只是另一种单文件源代码程序。如前所示,我们可以直接使用源代码启动器运行一个紧凑源文件,或者我们可以显式编译它然后运行它。
javadoc 工具可以从紧凑源文件生成文档,尽管隐式声明的类不应被其他类引用,因此不能用于定义 API。记录隐式声明类的成员可能对学习 javadoc 的初学者以及为最终用于大型程序的代码进行原型设计的经验丰富的开发者很有用。
与控制台交互
初学者经常编写与控制台交互的程序。写入控制台应该是直截了当的,但传统上这需要调用难以理解的 System.out.println 方法。对于初学者来说,这非常神秘:什么是 System?什么是 out?
更糟糕的是从控制台读取,这同样应该是一个直截了当的方法调用。由于写入控制台涉及使用 System.out,似乎从控制台读取理应涉及使用 System.in,但从 System.in 获取 String 需要很多代码,例如:
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String line = reader.readLine();
...
} catch (IOException ioe) {
...
}
有经验的开发者习惯了这种样板代码,但对于初学者来说,这段代码包含了更多的谜团,导致了大量问题:try 和 catch 是什么,BufferedReader 是什么,InputStreamReader 是什么,IOException 又是什么?还有其他方法,但没有一种对初学者来说明显更好。
为了简化交互式程序的编写,我们添加了一个新的 java.lang.IO 类,它声明了五个静态方法:
public static void print(Object obj);
public static void println(Object obj);
public static void println();
public static String readln(String prompt);
public static String readln();
现在初学者可以编写 Hello, World! 如下:
void main() {
IO.println("Hello, World!");
}
然后他们可以轻松地进入最简单的交互式程序:
void main() {
String name = IO.readln("Please enter your name: ");
IO.print("Pleased to meet you, ");
IO.println(name);
}
初学者确实需要学习这些基本的面向行的 I/O 方法需要 IO 限定符,但这并不是一个过度的教学负担。他们很可能很快就会学习到类似的限定符,例如用于 Math.sin(x) 等数学函数的 Math 限定符。
由于 IO 类位于 java.lang 包中,它可以在任何 Java 程序中无需 import 声明即可使用。这适用于所有程序,而不仅仅是紧凑源文件或声明实例 main 方法的程序;例如:
class Hello {
public static void main(String[] args) {
String name = IO.readln("Please enter your name: ");
IO.print("Pleased to meet you, ");
IO.println(name);
}
}
自动导入 java.base 模块
Java 平台 API 中的许多其他类在小型程序中也很有用。它们可以在紧凑源文件开头显式导入:
import java.util.List;
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
IO.println(name + ": " + name.length());
}
}
有经验的开发者会觉得这很自然,尽管为了方便,有些人可能会倾向于使用按需导入声明(即 import java.util.*)。然而,对于初学者来说,任何形式的 import 都是另一个神秘的来源,需要理解 Java API 的包层次结构。
为了进一步简化小型程序的编写,我们让 java.base 模块导出的包中的所有公共顶层类和接口都可以在紧凑源文件中使用,就像它们是按需导入的一样。因此,java.io、java.math 和 java.util 等常用包中的流行类和接口可以立即使用。在上面的例子中,import java.util.List 可以被移除,因为 List 将被自动导入。
一个配套的 JEP 提出了一个新的导入声明类型 import module M,它按需导入模块 M 导出的所有公共顶层类和接口。每个紧凑源文件都被视为自动导入 java.base 模块,就像 import module java.base; 声明出现在每个紧凑源文件的开头一样。
程序的演进
紧凑源文件中的小型程序专注于程序的功能,省略了它不需要的概念和构造。尽管如此,所有成员都像在普通类中一样被解释。要将一个紧凑源文件演变成一个普通源文件,我们只需要将其字段和方法封装在一个显式的 class 声明中,并添加一个导入声明。例如,这个紧凑源文件:
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
IO.println(name + ": " + name.length());
}
}
可以演变成一个声明单个类的普通源文件:
import module java.base;
class NameLengths {
void main() {
var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
for (var name : authors) {
IO.println(name + ": " + name.length());
}
}
}
main 方法没有任何改变。因此,将小型程序转变为可以在大型程序中充当组件的类始终是直截了当的。
替代方案
自动导入控制台 I/O 方法
在该功能的早期预览版中,我们探索了紧凑源文件自动导入新的 IO 类的静态方法的可能性。这样,开发者可以在紧凑源文件中编写 println(...) 而不是 IO.println(...)。
这有一个令人愉悦的效果,使得 IO 中的方法看起来像是 Java 语言的内置方法,但这给入门坡道增加了一个减速带:要将一个紧凑源文件演变成一个普通源文件,初学者将不得不添加一个 static import 声明 —— 这是另一个高级概念。这与我们的第二个目标相悖,即初学者应该能够优雅地扩展他们的代码。这种设计还会带来长期的负担,即审阅源源不断地提议向 IO 类添加额外方法的提议。
自动导入更少的包
我们不是自动导入 java.base 模块中的所有 54 个包到紧凑源文件中,而是可以只导入其中的一部分。但是,是哪些呢?
每个读者都会对自动导入到每个小型程序中的包有自己的建议:java.io 和 java.util 会是普遍的建议;java.util.stream 和 java.util.function 会很常见;而 java.math、java.net 和 java.time 也各有支持者。对于 JShell 工具,我们设法找到了十个在进行一次性 Java 代码实验时普遍有用的 java.* 包,但很难看出 java.base 包的哪个子集应该永久且自动地导入到每个紧凑源文件中。此外,这个列表会随着 Java 平台的发展而变化;例如,java.util.stream 和 java.util.function 仅在 Java 8 中引入。开发者可能会依赖 IDE 来提醒他们哪些自动导入是有效的 —— 这是一个不理想的结果。
导入 java.base 模块导出的所有包,对于紧凑源文件隐式声明的类来说,是一个一致且合理的选择。
允许顶层语句
另一种设计是允许语句直接出现在紧凑源文件中,从而无需声明 main 方法。这种设计会将整个紧凑源文件解释为一个隐式声明的类的隐式声明的 main 方法的主体。
不幸的是,这种设计会受到限制,因为它将无法在紧凑源文件中声明方法。这些方法将被解释为出现在一个不可见 main 方法的主体中,但这将使它们不合法,因为方法不能在方法内部声明。紧凑源文件将只能表示由一个接一个语句组成的线性程序,而没有将重复计算抽象为子程序的能力。
此外,在这种设计中,所有变量声明都将被解释为不可见 main 方法的局部变量。这将受到限制,因为局部变量只有在实际上是 final 时才能从 lambda 表达式中访问,这是一个高级概念。在紧凑源文件中编写 lambda 表达式会容易出错且令人困惑。
我们相信,直接在紧凑源文件中编写语句、脱离方法主体的愿望,主要是由编写 public static void main(String[] args) 的痛苦所驱动的。既然我们已经使 main 方法更容易声明,我们相信紧凑源文件最好由方法和字段组成,而不是语句。
扩展 JShell
JShell 是一个用于立即执行 Java 代码的交互式工具。它提供了一个增量编程环境,允许初学者无需太多繁琐步骤即可进行实验。
另一种设计是扩展 JShell 以实现我们的目标。虽然这在理论上是一个有吸引力的想法,但在实践中却不那么吸引人。
JShell 会话不是一个 Java 程序,而是一系列代码片段。片段一个接一个地执行,但它们不是独立的:当前片段的执行依赖于所有先前片段的执行结果,因此值和声明看起来是随时间演变的。在任何一个时刻,都有一个正在开发的程序的当前状态概念,但没有实际的程序文本表示。这对于实验(JShell 的主要用例)来说效果很好,但它不是帮助初学者编写真实程序的现实基础。
在更技术层面上,JShell 会话中的所有声明都被解释为未指定类的 static 成员,所有语句都在一个所有先前声明都在作用域内的上下文中执行。如果我们将一个紧凑源文件解释为一系列代码片段,那么该文件将只能表达其方法和字段都是 static 的类,实际上是引入了一种Java 方言。将一个紧凑源文件演变成一个普通源文件将涉及向每个方法和字段声明添加 static 修饰符,这阻碍了小型程序向大型程序的优雅演进。
引入一种新的 Java 语言方言
一个截然不同的设计将为紧凑源文件定义一种不同的语言方言。这将允许为了简洁而移除各种东西。例如,我们可以取消 main 方法必须显式声明为 void 的要求。不幸的是,这将阻止小型程序向大型程序的优雅演进,这是一个更重要的目标。我们更喜欢入门坡道,而不是悬崖边。
更多推荐



所有评论(0)