前言
半天前,reddit 上一篇名为 “Oracle trying to troll us.”的帖子突然爆火,随后这张图片被传入中文互联网,同样引发了网友热烈的讨论。这篇帖子的文章内容只有这样一张图片:
如果你是一位苦逼的 Java 程序员,那么当你看到这张图的时候也许震惊的会跳起来,但是如果你没有看懂,那就且听我细细往下说......
JEP 445 的前世今生
JEP 445: Unnamed Classes and Instance Main Methods (Preview) 的标题翻译过来是 “未命名类和实例 main 方法”,仅看标题你可能并不认为和上面那些东西有什么关系,但事实上,上述特性确实是由此 JEP 带来的。
事实上,JEP 445 早在 2023 年 2 月就被创建了,单之所以刚刚才火,是因为 OpenJDK 14 个小时前才批准了这个 JEP 的代码实现:JDK-8306112 Implementation of JEP 445: Unnamed Classes and Instance Main Methods (Preview) by JimLaskey · Pull Request #13689 · openjdk/jdk (github.com)
值得一提的是,JEP 445 是一个即将在 Java 21 中引入的预览( preview )提案,这意味着你需要通过在编译和运行时传入 --release 21 和 --enable-preview 命令行参数才能体验到这个功能
这种简化写法并不是 Java 的特例,其实早在 .NET 6 ,C# 就引入了一套 "控制台模板" 语法,其允许你在 C# 的主类文件(这里是 Program.cs)这么写:
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
其等价于:
using System;
namespace MyApp // Note: actual namespace depends on the project name.
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
很神奇对不对,但实际上说简单点这只是套语法糖而已。那么,JEP 445 也是如此吗?答案是否定的,甚至,它连语法糖都没有引入。
真的是变天了吗?
如果你仔细查看 JEP 提案的原文,你会发现他们在 Summary 和 Goal 上提到最多的两个词是:students 和 beginners:
而仔细读读这部分内容你会知道,这个 JEP 设立的初衷是为了为学生和 Java 新手隐去晦涩难懂的部分,仅保留一些简单的语法,方便他们快速入门和学习 Java ,但并不是引入了一套额外的 Java 方言。
从始至终,这套东西就不是给普通 Java 开发者使用的,而是面向学生和新手入门使用的。
那么,JEP 445 到底引入了一套什么样的机制呢?
未命名类和实例 main 方法
JEP 445 引入了如下两个机制:Unnamed Classes 和 Instance Main Methods,通过如下两个机制,简化了 main 方法的声明。让我们先从后者开始讲起。
实例 main 方法
首先,我们来看如下代码:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
一个非常经典的“Hello World”代码,一个 HelloWorld.java 文件中包含了一个 HelloWorld 类,其中包含一个公开的静态 main 方法,并包含 args 形参;方法体内调用 System.out.println 方法打印 Hello, World! 到标准输出中。
这无疑非常复杂,以至于我用自然语言描述这段代码都用了三行字。而最烦人的是,这是 main 方法唯一的写法,你没有任何办法简化这套代码,哪怕像 C 语言那样隐去 args 形参都不行。
为了解决这个问题,JEP 445 引入了一套“灵活的启动协议( flexible launch protocol )”。首先,这允许“实例 main 方法”存在,所谓“实例 main 方法”,就是指“非静态的 main 方法”,这意味着,main 方法将可以是 non-static 的;接着一个 main 方法的访问修饰符将不必是 public 的,只需要是 non-private(也即public, protected 和 package-protected)的即可;最后,main 方法中的 String[] args 将是可选传入的。这意味着,你现在可以将代码简化到如下程度:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
因此,上述代码从来就不是什么新的语法糖,而是我们所熟知的东西:一个 HelloWorld.java 文件中包含了一个 HelloWorld 类,其中包含一个包访问级别的非静态 main 方法,不包含形参;方法体内调用 System.out.println 方法打印 Hello, World! 到标准输出中。
非常有意思对不对,而如果存在多个 main 方法,将会以如下的优先级,选择优先级最高的一个 main 调用:
[ol]
[/ol]
这其实改变了 Java 原有的行为:如果一个启动类声明了一个非静态的 main 方法,同时其超类存在一个“传统的”public static void main(String[] args) 方法,那么现在 Java 将会调用前者,而不是后者(当然,如果你真的这么做了,JVM 会在运行时输出一个警告来提示你)。
最后,如果一个即将被调用的 main 方法是一个内部类的成员,那么程序将无法运行。
所以,JEP 445 事实上是通过一系列语法层面的让步引入了一套更加方便使用的 main 方法模板,而并不是引入了一套新的语法或是语法糖。
未命名类
也许你早已知道,当一个 Java 类文件位于源代码文件的顶级,也就是说其不属于任何包中时,那我们就说这个类属于一个“未命名包”。在 JEP 445 中,引入了“未命名类”的概念,当一个类源代码中不包含任何类声明,而仅有方法声明和成员变量声明时,该类便被称为“未命名类”。
未命名类永远是未命名包的成员,而且其永远是 final 的,也就是说其不能实现或拓展任何接口和类;未命名类无法使用静态方法的方法引用,但是仍然可以使用 this 关键字或非静态方法的方法引用。
未命名类不能被其他类按名称引用,也无法构造其实例;其内部写法与显式声明的类完全相同,除了其只能有一个默认的无参构造方法。
通过引入未命名类,上述代码最终可以被简化成这样:
void main() {
System.out.println("Hello, World!");
}
一个未命名类,其中包含一个包访问级别的非静态 main 方法,不包含形参;方法体内调用 System.out.println 方法打印 Hello, World! 到标准输出中。
除此之外,一个未命名类依然可以拥有成员变量和成员方法,例如这样:
String greetingMsg = "Hello, World!";
String greeting() { return greetingMsg; }
void main() {
System.out.println(greeting());
}
当 JVM 试图执行一个在一个未命名类中的非静态 main 方法时,实际上等同于创建了一个匿名类,然后再执行方法:
new Object() {
// the unnamed class's body
}.main();
我们可以通过 java 指令来直接运行一个未命名类源代码,像是这样:
$ java HelloWorld.java
然后,Java 编译器会将其编译为 HelloWorld.class,找到 main 方法并执行。即使这里我们给未命名类分配了一个名字,但是这个名字实际上是不能用在 Java 源代码中的。
最后,在当前预览版本中,如果我们的 Java 代码中含有未命名类,那么 javadoc 实用工具将无法生成 API 文档,因为其本身就无法被其他类访问。
后记
看完整个 JEP ,我只想感叹 OpenJDK 开发者的脑洞确实是大,竟然通过引入两套新的机制,巧妙地解决了 Java main 方法冗长的问题,而并未引入新的语法或语法糖,以造成用户体验割裂。
这篇 Reddit 文章下的高赞评论给出了 JEP 445 的链接,随后提问到:“这将是 Java 模板代码梗的末日吗”,我想,至少在 JEP 445 中,这种痛苦还远未结束吧。(完)
引用