Skip to content

JEP 463: Implicitly Declared Classes and Instance Main Methods (Second Preview) | 隐式声明的类和实例主方法(第二次预览)

摘要

对 Java 编程语言进行改进,以便学生在无需理解为大型程序设计的语言特性的情况下编写他们的第一个程序。学生无需使用单独的语言方言,他们可以为单类程序编写精简的声明,然后随着技能的增长无缝扩展他们的程序以使用更高级的特性。这是一个 预览语言特性

历史

JEP 445 提出了“未命名类和实例 main 方法”,它在 JDK 21 中进行了预览。反馈表明该特性应该在 JDK 22 中再次进行预览,并进行以下重大更改,因此有了修订后的标题。

  • 允许类未命名以及允许没有封闭类声明的源文件隐式声明一个未命名类的想法主要是一种规范手段,以确保该类不能被其他类使用。然而,这已被证明是一种干扰。我们采用了一种更简单的方法:没有封闭类声明的源文件被认为是隐式声明了一个由宿主系统选择名称的类。这样隐式声明的类的行为与普通顶级类一样,不需要额外的工具、库或运行时支持。

  • 选择要调用的 main 方法的过程过于复杂,需要同时考虑该方法是否有参数以及它是静态方法还是实例方法。在这次第二次预览中,我们提议将选择过程简化为两步:如果有一个带有 String[] 参数的候选 main 方法,那么我们调用那个方法;否则,我们调用一个没有参数的候选 main 方法。这里没有歧义,因为一个类不能声明具有相同名称和签名的静态方法和实例方法。

目标

  • 为 Java 编程提供一个平稳的入门途径,以便教师可以逐步介绍概念。

  • 帮助学生以简洁的方式编写基本程序,并随着技能的增长优雅地扩展他们的代码。

  • 减少编写简单程序(如脚本和命令行实用程序)的繁琐程度。

  • 不引入单独的 Java 初学者方言。

  • 不引入单独的初学者工具链;学生程序应该使用与编译和运行任何 Java 程序相同的工具进行编译和运行。

动机

Java 编程语言在由大型团队多年开发和维护的大型、复杂应用程序中表现出色。它具有丰富的数据隐藏、重用、访问控制、命名空间管理和模块化特性,允许组件在独立开发和维护的同时进行干净的组合。有了这些特性,组件可以为与其他组件的交互公开明确的接口,同时隐藏内部实现细节,以便每个组件都能独立演进。实际上,面向对象范式本身就是为了通过明确的协议将各个部分组合在一起并抽象出实现细节而设计的。这种大型组件的组合被称为“大规模编程”。该语言还提供了许多对“小规模编程”有用的构造——即组件内部的所有内容。近年来,我们通过 模块 增强了它的大规模编程能力,并通过 面向数据编程 增强了它的小规模编程能力。

然而,Java 编程语言也旨在成为第一门语言。当程序员刚开始时,他们不是在团队中编写大型程序——他们独自编写小型程序。他们不需要封装和命名空间,这对于分别演进由不同人编写的组件很有用。在教授编程时,教师从变量、控制流和子例程等基本的小规模编程概念开始。在那个阶段,不需要类、包和模块等大规模编程概念。使语言对新手更友好符合 Java 老手的利益,但他们也可能会发现更简洁地编写简单程序很愉快,而无需任何大规模编程的脚手架。

考虑经典的 “Hello, World!” 程序,它经常被用作 Java 学生的第一个程序:

java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

对于这个程序所做的事情来说,这里有太多的杂乱——太多的代码、太多的概念、太多的构造。

  • class 声明和强制的 public 访问修饰符是大规模编程构造。当用明确的外部组件接口封装一个代码单元时它们很有用,但在这个小例子中毫无意义。

  • String[] args 参数的存在也是为了使代码与外部组件(在这种情况下是操作系统的外壳)进行接口。在这里它很神秘且没有帮助,特别是因为在像 HelloWorld 这样的简单程序中它没有被使用。

  • static 修饰符是语言的类和对象模型的一部分。对于新手来说,static 不仅神秘而且有害:要添加更多 main 可以调用和使用的方法或字段,学生必须要么将它们全部声明为 static——从而传播一种既不常见也不是好习惯的习惯——要么面对静态和实例成员之间的差异并学习如何实例化一个对象。

新程序员在最糟糕的时候遇到这些概念,在他们学习变量和控制流之前,并且当他们无法理解大规模编程构造对于保持大型程序良好组织的实用性时。教师经常发出警告,“别担心那个,你以后会理解的。”这对他们和他们的学生来说都不令人满意,并给学生留下了语言很复杂的持久印象。

这个 JEP 的动机不仅仅是减少繁琐。我们的目标是帮助新接触 Java 语言或一般编程的程序员以一种正确引入概念的方式学习语言:从基本的小规模编程概念开始,然后在实际有益且更容易理解的时候进行高级的大规模编程概念。

我们提议不是通过改变 Java 语言的结构来做到这一点——代码仍然在方法中,方法在类中,类在包中,包在模块中——而是通过隐藏这些细节直到它们在较大的程序中有用。我们提供一个入门途径,一个逐渐上升的斜坡,优雅地并入高速公路。当学生转向更大的程序时,他们不必丢弃他们在早期阶段学到的东西,而是看到这一切如何融入更大的图景。

我们在这里提供的更改只是使 Java 语言更易于学习的一步。它们甚至没有解决“Hello, World!”程序中的所有障碍:初学者可能仍然对神秘的 System.out.println 咒语感到困惑,并且即使在第一周的程序中仍然需要导入基本的实用类以获得基本功能。我们可能会在未来的 JEP 中解决这些问题。

描述

首先,我们增强了启动 Java 程序的协议以允许 实例 main 方法。这样的方法不是 static 的,不需要是 public 的,也不需要有 String[] 参数。然后我们可以将“Hello, World!”程序简化为:

java
class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

其次,我们允许一个 编译单元,即一个源文件,隐式声明 一个类:

java
void main() {
    System.out.println("Hello, World!");
}

这是 预览语言特性,默认禁用。

要在 JDK 22 中尝试下面的示例,你必须按如下方式启用预览特性:

  • 使用 javac --release 22 --enable-preview Main.java 编译程序,并使用 java --enable-preview Main 运行它;或者,

  • 当使用 源代码启动器 时,使用 java --source 22 --enable-preview Main.java 运行程序。

启动协议

新程序员只是想编写并运行一个计算机程序。然而,《Java 语言规范》 主要关注定义 Java 的核心单元类和基本编译单元,即一个源文件,由一个 package 声明开头,接着是一些 import 声明,然后是一个或多个 class 声明。它对 Java程序 所说的只是 这个

Java 虚拟机通过调用某个指定类或接口的 main 方法开始执行,向它传递一个单一的参数,该参数是一个字符串数组。

JLS 进一步说:

在 Java 虚拟机中指定初始类或接口的方式超出了本规范的范围,但在使用命令行的宿主环境中,通常将类或接口的完全限定名称指定为命令行参数,并将后续的命令行参数用作字符串提供给 main 方法的参数。

选择包含 main 方法的类、以模块路径或类路径(或两者)的形式组装其依赖项、加载该类、初始化它,并使用其参数调用 main 方法的这些动作构成了 启动协议。在 JDK 中,它由 启动器(即 java 可执行文件)实现。

灵活的启动协议

我们增强了启动协议,以在程序入口点的声明中提供更多灵活性,特别是允许 实例main 方法,如下所示:

  • 允许启动的类的 main 方法具有 publicprotected 或默认(即包)访问权限。

  • 如果启动的类包含一个带有 String[] 参数的 main 方法,那么选择那个方法。

  • 否则,如果该类包含一个没有参数的 main 方法,那么选择那个方法。

  • 在任何一种情况下,如果选择的方法是 static,那么直接调用它。

  • 否则,选择的方法是一个实例方法,并且启动的类必须有一个无参数、非 private 的构造函数(即具有 publicprotected 或包访问权限)。调用那个构造函数,然后调用结果对象的 main 方法。如果没有这样的构造函数,则报告错误并终止。

  • 如果没有合适的 main 方法,则报告错误并终止。

这些更改允许我们编写没有访问修饰符、没有 static 修饰符且没有 String[] 参数的“Hello, World!”,因此可以推迟引入这些构造,直到需要它们的时候:

java
class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

隐式声明的类

在 Java 语言中,每个类都位于一个包中,每个包都位于一个模块中。这些命名空间和封装构造适用于所有代码,但不需要它们的小程序可以省略它们。一个不需要类命名空间的程序可以省略 package 语句,使其类成为未命名包的隐式成员;未命名包中的类不能被命名包中的类显式引用。一个不需要封装其包的程序可以省略模块声明,使其包成为未命名模块的隐式成员;未命名模块中的包不能被命名模块中的包显式引用。

在类作为对象构造的模板发挥其主要作用之前,它们仅作为方法和字段的命名空间。在学生对变量、控制流和子例程等更基本的构建块感到舒适之前,在他们开始学习面向对象之前,以及当他们仍在编写简单的单文件程序时,我们不应该要求他们面对类的概念。即使每个方法都位于一个类中,对于不需要它的代码,我们可以不再要求显式的类声明——就像对于不需要它们的代码我们不要求显式的包或模块声明一样。

从今以后,如果 Java 编译器遇到一个源文件,其中有一个方法没有被封闭在类声明中,那么它将把那个方法、任何其他这样的方法以及任何未封闭的字段和文件中的任何类视为一个 隐式声明 的顶级类的主体。

这样的隐式声明的类(或简称为 隐式类)始终是未命名包的成员。它也是 final 的,不实现任何接口,也不扩展除 Object 之外的任何类。隐式类不能通过名称引用,所以不能对其静态方法有 方法引用;然而,this 关键字仍然可以使用,对实例方法的方法引用也可以使用。

隐式类的代码不能通过名称引用隐式类,所以不能直接构造隐式类的实例。这样的类仅在作为独立程序或作为程序的入口点时有用。因此,隐式类必须有一个可以如上所述启动的 main 方法。这个要求由 Java 编译器强制执行。

隐式类位于未命名包中,未命名包位于未命名模块中。虽然只能有一个未命名包(不考虑多个类加载器)和一个未命名模块,但未命名模块中可以有多个隐式类。每个隐式类都包含一个 main 方法,因此代表一个程序,所以未命名包中的多个这样的类代表多个程序。

隐式类几乎与显式声明的类完全一样。它的成员可以有相同的修饰符(例如 privatestatic),并且修饰符有相同的默认值(例如包访问权限和实例成员资格)。一个关键区别是,虽然隐式类有一个默认的无参数构造函数,但它不能有其他构造函数。

有了这些更改,我们现在可以这样编写“Hello, World!”:

java
void main() {
    System.out.println("Hello, World!");
}

顶级成员被解释为隐式类的成员,所以我们也可以这样编写程序:

java
String greeting() { return "Hello, World!"; }

void main() {
    System.out.println(greeting());
}

或者,使用一个字段,如下所示:

java
String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}

如果一个隐式类有一个实例 main 方法而不是一个 static main 方法,那么启动它等同于下面的代码,它使用现有的 匿名类声明 构造:

java
new Object() {
    // 隐式类的主体
}.main();

一个名为 HelloWorld.java 的包含隐式类的源文件可以使用源代码启动器启动,如下所示:

shell
$ java HelloWorld.java

Java 编译器将把那个文件编译为可启动的类文件 HelloWorld.class。在这种情况下,编译器选择 HelloWorld 作为类名是一个实现细节,但那个名称仍然不能在 Java 源代码中直接使用。

javadoc 工具不能为隐式类生成 API 文档,因为隐式类没有定义任何可从其他类访问的 API,但隐式类的字段和方法可以生成 API 文档。

扩展程序

作为隐式类编写的“Hello, World!”程序更加关注程序实际做的事情,省略了它不需要的概念和构造。即便如此,所有成员的解释方式与在普通类中完全一样。要将一个隐式类演变为一个普通类,我们只需要将其声明(不包括 import 语句)包裹在一个显式的 class 声明中。

完全消除 main 方法似乎是自然的下一步,但这会违背将第一个 Java 程序优雅地扩展为更大程序的目标,并且会施加一些不明显的限制(见 下面)。删除 void 修饰符同样会创建一个不同的 Java 方言。

替代方案

  • 使用 JShell 进行入门编程——一个 JShell 会话不是一个程序,而是一系列代码片段。输入到 jshell 中的声明被隐式地视为某个未指定类的静态成员,具有一些未指定的访问级别,并且语句在一个所有先前声明都在作用域内的上下文中执行。

    这对于实验很方便——这是 JShell 的主要用例——但不是学习编写 Java 程序的好模型。将 JShell 中的一批有效的声明演变为一个真正的 Java 程序会导致一种非惯用法的代码风格,因为它将每个方法、类和变量都声明为 static。JShell 是一个很好的探索和调试工具,但它不是我们正在寻找的入门编程模型。

  • 将代码单元解释为静态成员——方法和字段默认情况下是非 static 的。将隐式类中的顶级成员解释为 static 会改变这样一个类中的代码单元的含义——实际上引入了一种不同的 Java 方言。为了在我们将隐式类演变为普通类时保留这些成员的含义,我们必须添加显式的 static 修饰符。这不是我们从少量方法扩展到一个简单类时想要的。我们想开始将类用作类,而不是静态成员的容器。

  • 将代码单元解释为局部变量——我们已经可以在方法中声明局部变量。假设我们也可以声明局部方法,即其他方法中的方法。然后我们可以将一个简单程序的主体解释为一个 main 方法的主体,将变量解释为局部变量而不是字段,将方法解释为局部方法而不是类成员。这将允许我们完全省略 main 方法并编写顶级语句。

    这种方法的问题是,在 Java 语言中,局部变量的行为与字段不同,并且以一种更受限制的方式:当局部变量是 有效最终的 时,它们只能在 lambda 表达式体或内部类中被访问。所提出的设计允许我们以与以往相同的方式将局部变量和字段分开。即使对于新学生来说,编写一个 main 方法的负担也并不繁重。

  • 引入包级别的方法和字段——通过允许在没有显式的 packageclass 声明的文件中声明包级别的方法和字段,可以实现与上述类似的用户体验。然而,这样的特性将对 Java 代码的一般编写方式产生更广泛的影响。