跳到主要内容

JEP 463:隐式声明的类和实例主要方法(第二预览版)

概括

发展 Java 编程语言,以便学生可以编写他们的第一个程序,而无需了解为大型程序设计的语言功能。学生无需使用单独的语言方言,而是可以为单类程序编写简化的声明,然后随着技能的增长无缝扩展他们的程序以使用更高级的功能。这是预览语言功能

历史

JEP 445提出了_未命名类和实例main方法_,该功能已在 JDK 21 中预览。反馈表明该功能应在 JDK 22 中第二次预览,并进行以下重大更改,因此需要修改标题。

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

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

目标

  • 为 Java 编程提供一个平稳的入门通道,以便教师可以循序渐进地介绍概念。

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

  • 减少编写简单程序(例如脚本和命令行实用程序)的仪式。

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

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

动机

Java 编程语言非常适合大型团队多年来开发和维护的大型复杂应用程序。它具有丰富的数据隐藏、重用、访问控制、命名空间管理和模块化功能,允许组件在独立开发和维护的同时干净地组合。借助这些功能,组件可以公开定义良好的接口,以便与其他组件交互,同时隐藏内部实现细节,以便允许每个组件独立发展。事实上,面向对象范式本身就是为了将通过明确定义的协议进行交互的各个部分插入在一起而设计的,并抽象出实现细节。这种大型组件的组合称为_大型编程_。该语言还提供了许多对_小型编程_有用的结构——组件内部的一切。近年来,我们既增强了模块的大编程能力,又增强了面向数据编程的小编程能力。

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

考虑一下经典的《你好,世界!》经常被Java学生用作第一个程序的程序:

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

对于程序的功能来说,这里有太多的混乱——太多的代码、太多的概念、太多的构造。

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

  • String[] args参数还用于将代码与外部组件(在本例中为操作系统的 shell)连接。它在这里很神秘且没有帮助,特别是因为它不用于像HelloWorld.

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

新程序员在最糟糕的情况下遇到这些概念,在他们了解变量和控制流之前,并且在他们无法理解大型编程结构对于保持大型程序良好组织的效用时。导师经常告诫我们:“别担心,稍后你就会明白的。”这让他们和他们的学生都不满意,并给学生留下了语言复杂的持久印象。

这个 JEP 的动机不仅仅是为了减少仪式。我们的目标是帮助刚刚接触 Java 语言或一般编程的程序员以按正确顺序介绍概念的方式学习该语言:从基本的小型编程概念开始,然后继续高级当大型概念确实有益并且更容易掌握时,就可以使用它们。

我们建议不要通过改变 Java 语言的结构来做到这一点——代码仍然包含在方法中,方法包含在类中,类包含在包中,包包含在模块中——而是隐藏这些细节,直到它们在更大的程序。我们提供一个入口匝道,一个逐渐倾斜的斜坡,优雅地融入高速公路。当学生进入更大的项目时,他们不需要放弃他们在早期阶段学到的东西,而是他们会看到这一切如何融入更大的图景。

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

描述

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

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

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

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语言规范package侧重于定义类的核心 Java 单元和基本编译单元,即由声明、后跟一些import声明、后跟一个或多个声明组成的源文件class。关于 Java_程序_,它所要说就是:

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

JLS 进一步指出:

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

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

灵活的启动协议

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

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

  • 如果启动的类包含main带有String[]参数的方法,则选择该方法。

  • 否则,如果类包含main不带参数的方法,则选择该方法。

  • 无论哪种情况,如果选择了方法,static则只需调用它即可。

  • 否则,所选方法是实例方法,并且启动的类必须具有零参数、非private构造函数(即 of publicprotected或包访问)。调用该构造函数,然后调用main结果对象的方法。如果不存在这样的构造函数则报告错误并终止。

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

这些更改使我们能够编写_Hello, World!_没有访问修饰符,没有static修饰符,也没有String[]参数,因此可以推迟这些构造的引入,直到需要它们为止:

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

隐式声明的类

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

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

从今以后,如果 Java 编译器遇到一个源文件,其中的方法未包含在类声明中,那么它将考虑该方法、任何其他此类方法以及文件中的任何未包含的字段和任何类,以形成隐式的主体_。宣布为_顶级班级。

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

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

隐式类驻留在未命名的包中,未命名的包驻留在未命名的模块中。虽然只能有一个未命名的包(除非有多个类加载器)和一个未命名的模块,但未命名的模块中可以有多个隐式类。每个隐式类都包含一个main方法,因此代表一个程序,因此未命名包中的多个此类代表多个程序。

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

通过这些更改,我们现在可以编写_Hello, World!_作为:

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

顶级成员被解释为隐式类的成员,因此我们也可以将程序编写为:

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

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

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

String greeting = "Hello, World!";

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

如果隐式类具有实例main方法而不是static主方法,则启动它相当于以下内容,它采用现有的匿名类声明构造:

new Object() {
// the implicit class's body
}.main();

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

$ java HelloWorld.java

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

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

发展一个计划

你好_,世界!_作为隐式类编写的程序更关注程序实际执行的操作,省略不需要的概念和构造。即便如此,所有成员的解释都与普通班级中的情况相同。要将隐式类演变成普通类,我们所需要做的就是将其声明(不包括import语句)包装在显式class声明中。

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

备择方案

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

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

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

  • 将代码单元解释为局部变量——我们已经可以在方法中声明局部变量。假设我们还可以声明本地方法,即其他方法中的方法。然后我们可以将简单程序的主体解释为方法的主体main,将变量解释为局部变量而不是字段,将方法解释为局部方法而不是类成员。这将使我们能够main完全避开该方法并编写顶级语句。

    这种方法的问题在于,在 Java 语言中,局部变量的行为与字段不同,并且以更受限制的方式启动:局部变量只能从 lambda 体或内部类内部访问(当它们实际上是 final时)。拟议的设计使我们能够像以往一样将当地人和田野分开。编写方法的负担main并不繁重,即使对于新学生来说也是如此。

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