Java 的 main 方法声明终于要变天了吗? —— 浅谈 JEP 445

2023-06-05 01:35:22 +08:00
 HikariLan

Java 的 main 方法声明终于要变天了吗? —— 浅谈 JEP 445

前言

半天前,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 上提到最多的两个词是:studentsbeginners:

而仔细读读这部分内容你会知道,这个 JEP 设立的初衷是为了为学生和 Java 新手隐去晦涩难懂的部分,仅保留一些简单的语法,方便他们快速入门和学习 Java ,但并不是引入了一套额外的 Java 方言

从始至终,这套东西就不是给普通 Java 开发者使用的,而是面向学生和新手入门使用的。

那么,JEP 445 到底引入了一套什么样的机制呢?

未命名类和实例 main 方法

JEP 445 引入了如下两个机制:Unnamed ClassesInstance 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, protectedpackage-protected)的即可;最后,main 方法中的 String[] args 将是可选传入的。这意味着,你现在可以将代码简化到如下程度:

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

因此,上述代码从来就不是什么新的语法糖,而是我们所熟知的东西:一个 HelloWorld.java 文件中包含了一个 HelloWorld 类,其中包含一个包访问级别非静态 main 方法,不包含形参;方法体内调用 System.out.println 方法打印 Hello, World! 到标准输出中。

非常有意思对不对,而如果存在多个 main 方法,将会以如下的优先级,选择优先级最高的一个 main 调用:

  1. 一个在启用类中声明,采用 non-private 访问级别的 static void main(String[] args) 方法;
  2. 一个在启动类中声明,采用 non-private 访问级别的 static void main() 方法;
  3. 一个在启动类中声明,或从其超类中继承的,采用 non-private 访问级别的 void main(String[] args) 方法;
  4. 一个在启动类中声明,或从其超类中继承的,采用 non-private 访问级别的 void main() 函数。

这其实改变了 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 中,这种痛苦还远未结束吧。(完)

引用

5286 次点击
所在节点    分享创造
41 条回复
Aloento
2023-06-05 01:56:02 +08:00
可以,支持
noreplay
2023-06-05 07:41:01 +08:00
也就是说当这个新特性铺开之后,Java 只会在自学刚开始爽,做工程还是那么便秘吗?
wangxiaodong
2023-06-05 08:21:11 +08:00
@noreplay 当前 Java 语法易用性已提升很大了,还有几个特性我比较关注,你可以了解下(虚拟线程、外部函数&内存 API) - https://congci.com/main/home/topics/java-programming/
Biluesgakki
2023-06-05 08:43:26 +08:00
@wangxiaodong 升级 jdk 是个大问题。。
enpitsulin
2023-06-05 10:17:24 +08:00
🤣主要是看到这些图就想到
enpitsulin
2023-06-05 10:18:57 +08:00
@enpitsulin 这些 before 和 after 的对比太搞了,实际上并不是这样的意思 简单一看真以为特性下放了😂
Senorsen
2023-06-05 11:19:00 +08:00
感觉这些年 Java 在努力追赶 C#、Scala 、Kotlin 、Go 等语言的特性啊,这么一看还是 Kotlin 之类的方便,升级新语法和特性,只需要更新个项目依赖就能享受到,底层还是基于 Java 字节码,而 Java 的升级还是太笨重了
boatrain1111
2023-06-05 11:54:56 +08:00
哪里复制的公众号文章
leonshaw
2023-06-05 12:12:36 +08:00
为什么不返回一个 int 呢?
K1W1
2023-06-05 12:16:09 +08:00
他强任他强,我用 java 8
zjp
2023-06-05 12:18:56 +08:00
#8 虽然 v 站上引流外站的很多,但是是不是原创自己搜一下内容就知道了。上来就说复制的真膈应人
vsitebon
2023-06-05 12:21:58 +08:00
@boatrain1111 这个就是作者
HikariLan
2023-06-05 13:48:29 +08:00
@boatrain1111 确实是自己写的啊哥们...
john6lq
2023-06-05 14:14:12 +08:00
简单说,就是支持 main 重载并以它作为启动入口了呗?
Masoud2023
2023-06-05 14:53:37 +08:00
扫了一眼这个 JEP ,还是觉得不要去为了解决问题而创造问题,如果仅仅是只有一个 main 函数可以放进这种 unnamed class ,那怎么继续解释为什么 Java 的其他方法不能也想这样直接脱离 class 写出来?本就可以两三句话说明白的事情,非得上升到这种层面上去,我觉得这个 JEP 仅仅是想找个话题随便水一下 JEP 而已。

喜欢这种函数式编程可以去写 python ,作为一个面向对象的语言,就不要去东施效颦去学面向过程的东西了。
HikariLan
2023-06-05 14:54:02 +08:00
@john6lq 应该说是支持非静态 main 方法启动了
HikariLan
2023-06-05 14:58:58 +08:00
@Masoud2023 可能是我没讲清楚?其他方法也是可以放进 unnamed class 里的,只不过没意义,因为只有 main 方法才能作为特例被启动(当然此时 main 方法调用 unnamed class 内的其他方法也是可行的)。

虽然但是,其实我个人也是觉得这个 JEP 蛮鸡肋的,用途不大。这篇文章只是因为看到社群中很多人认为这是 Java 21 的一个新语法糖进而产生了误解,故撰文解答之。
wangxiaodong
2023-06-05 14:59:24 +08:00
@HikariLan
简化的几乎可以当脚本使用了:

#!/usr/bin/java --source 17
void main() { System.out.println("Hello, World!"); }
wangxiaodong
2023-06-05 15:06:24 +08:00
@Masoud2023 Java 的虚拟线程其实也做到了对 Thread 的最大化兼容,这次的 JEP 我能 Get 到 @HikariLan 的关注点,就是尽量以改动最小的角度来改造 Java 。
HikariLan
2023-06-05 15:23:06 +08:00
@wangxiaodong 其实这种情况你可以使用 Java9 新增的 jshell 来写的(如果你注意原 JEP 最下面的 alternatives ,第一个就是 jshell )

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/945770

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX