跳到主要内容

JEP 445:未命名类和实例主要方法(预览)

概括

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

目标

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

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

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

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

  • 不要引入单独的初学者工具链;学生程序应该使用与编译和运行任何 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)连接。它在这里很神秘且无用,尤其是因为它从未被使用过。

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

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

这个 JEP 的动机不仅仅是为了减少仪式,而是为了帮助刚接触 Java 或一般编程的程序员以正确的顺序介绍概念的方式学习 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!");
}
}

其次,我们引入_未命名的类_来使class声明隐式:

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

这是预览语言功能,默认禁用

要在 JDK 21 中尝试以下示例,您必须启用预览功能,如下所示:

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

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

启动协议

新程序员想要编写和运行计算机程序,但Java 语言规范侧重于定义类的核心 Java 单元和基本编译单元,即由声明package、后跟一些import声明、后跟一个或多个声明组成的源文件。更多class声明。关于 Java_程序_,它所要说就是:

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

JLS 进一步指出:

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

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

灵活的启动协议

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

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

  • 如果启动的类不包含static main带参数的方法String[],但包含static main不带参数的方法,则调用该方法。

  • 如果启动的类没有static main方法,但具有private非零参数构造函数(即 of publicprotected或包访问)和非private实例main方法,则构造该类的实例。如果该类有main一个带参数的实例方法,String[]则调用该方法;否则,调用不带参数的实例main方法。

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

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

选择主要方法

启动类时,启动协议会选择以下方法中的第一个来调用:

  1. 在启动类中声明的static void main(String[] args)非私有访问方法(即,或public包),protected

  2. static void main()在启动类中声明的非私有访问方法,

  3. void main(String[] args)在启动的类中声明或从超类继承的非私有访问的实例方法,或者最后,

  4. void main()在启动的类中声明或从超类继承的非私有访问的实例方法。

请注意,这是行为的更改:如果启动的类声明了一个实例 main,则将调用该方法,而不是public static void main(String[] args)在超类中声明的继承的“传统”方法。因此,如果启动的类继承了“传统”的 main 方法,但选择了另一个方法(即实例main),JVM 将在运行时向标准错误发出警告。

如果选择的main是实例方法并且是内部类的成员,则程序将无法启动。

未命名类

在 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 unnamed class's body
}.main();

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

$ java HelloWorld.java

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

当要求为具有未命名类的 Java 文件生成 API 文档时,该javadoc工具将失败,因为未命名类不定义可从其他类访问的任何 API。此行为可能会在未来版本中发生变化。

Class.isSynthetic方法返回true一个未命名的类。

发展一个计划

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

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

备择方案

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

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

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

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

    这种方法的问题在于,在 Java 中,局部变量的行为与字段不同,并且以更受限制的方式启动:局部变量只能从 lambda 体或内部类内部访问(当它们实际上是 final时)。所提出的设计允许我们以与 Java 中通常所做的相同的方式分离局部变量和字段。编写方法的负担main并不繁重,即使对于新学生来说也是如此。

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