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

引用

5359 次点击
所在节点    分享创造
41 条回复
wangxiaodong
2023-06-05 15:30:46 +08:00
@HikariLan jshell 的用法、参数也是学习成本,我喜欢更广泛的 Linux Shebang 写法 #!/usr/bin/java --source 21

另外,我喜欢引入的与“未命名包”对称的“Unnamed Classes”概念,满足奥卡姆剃刀定律:如无必要,勿增实体。
netabare
2023-06-05 15:38:20 +08:00
感觉不如 async/await 或者 auto prop 。
Masoud2023
2023-06-05 15:57:45 +08:00
@HikariLan #17 好奇这种 unnamed class 铺开之后,没有 class 的 java 将来怎么组织 project structure🤣
allenzhangSB
2023-06-05 16:10:28 +08:00
你们这些人就不会好好写标题么?
nothingistrue
2023-06-05 16:23:32 +08:00
main 方法还好说,只是扩展了 JVM 执行入口,影响范围很小。未命名类就扯淡了,这增加了一个语法条款,但这条款仅仅是为 main 方法服务的。

这个措施,比编译替换语法糖可是要糟糕得多,架空 JCP 以及又额外搞出个 JEP 的 Oracle 就是能这么任性。
HikariLan
2023-06-05 16:35:15 +08:00
@Masoud2023 铺不开的,这玩意只是个方便新手学习的妥协罢了。
就像我们从来不用 unnamed package 一样
HikariLan
2023-06-05 16:35:42 +08:00
@allenzhangSB 唉,我也挺无奈的,正经点又没人看,只能尽量正文正经了
StevenQAQ
2023-06-05 17:02:44 +08:00
是好事,初学者难度降低,开发者也可以适当的使用简单的语法糖来处理一些简单的问题,方便高效。JEP445 是好东西。
wangxiaodong
2023-06-05 17:16:00 +08:00
@nothingistrue 既然有“未命名包”,完全可以有“未命名类”啊,一点理解负担都不会增加,而且还让人感觉 JEP 作者有始有终,将来出现个“未命名方法”,也是自然而然的啦! so ,并不是为 main 方法专一引入的。
nothingistrue
2023-06-05 17:29:33 +08:00
@wangxiaodong #29 楼主是中文描述,仔细看,不要凭空想象。

JEP 是 Oracle 与 2011 年左右成立的,它跟 “未命名包” 没有关系。
wangxiaodong
2023-06-05 17:35:48 +08:00
@nothingistrue 未命名包很早就有了,我就经常用,这次推出个未命名类,我觉得一脉相承,影响并不大。我不关心 JEP 和 Oracle 的关系,而只关心 Java 的实质改动,不太认同你所谓的“糟糕...任性”,反而我觉得这个 JEP 来的太迟了。
KevinBlandy
2023-06-05 18:01:10 +08:00
害,整了一阵子 golang ,倒喜欢上了 go 这种简洁的感觉。
FrankAdler
2023-06-05 22:00:03 +08:00
实际开发跟 main 函数打交道的时间 0.000000000000000000000000000001%都不到,所以真的不在乎这一点冗余了
nikenidage1
2023-06-05 22:11:00 +08:00
C# 2021 年就实装的特性,而且连 main 都不需要
wxlwsy
2023-06-05 22:41:55 +08:00
我不理解,有些人,多写几个单词好像就是原罪. 多个括号就是异端.
acerphoenix
2023-06-06 09:51:36 +08:00
很不赞成,java 的基础就是类,这概念入门并不难,静态 main 方法就是入口,多大多复杂工程也是这么个入口,一招鲜的概念。非得再搞不伦不类的第二种,入门不学规范写法,学个这么 trick 的写法,徒增认知负担。
wangxiaodong
2023-06-06 12:44:48 +08:00
@acerphoenix 使用者没任何负担和语法增加,就是少写个 class{}而已,JVM 解析辛苦点而已,但 JVM 不就是干脏活累活的嘛。别的不写类直接执行 println 函数的编程语言,是不是全部都要被你打上"徒增认知负担"!?
acerphoenix
2023-06-06 13:43:58 +08:00
@wangxiaodong #37 基本简单的标准,做一件事只有一种方式。要做减法,这倒是减了一两行代码,却要增加一种认知,只能说这个减法做的非常不聪明。以后还得学现有方式,你怎么说出没有任何负担的?照你说了这个也就少写个 class ,那现在的方式也就多写个 class ,能有多复杂?
别的语言不写类是因为本身就不用写,这里是多余的加了一种没必要的认知,这俩完全不一样的概念你是怎么想到放一起类比的?
wangxiaodong
2023-06-06 17:24:15 +08:00
@acerphoenix 没有说服我,但感谢你码字的时间,至少这个 JEP 是不可阻挡了。哈哈,🤝。
bv
2023-06-07 19:52:53 +08:00
@acerphoenix #38 你信不信,有了简化 main 的写法,极有可能的结果就是非但不能减少初学者的心智负担,相反还可能加重初学者的心智负担。以后很有可能会有以下这些新手八股文:main 函数的发展历史;简化 main 与 class main 有何异同;为什么要简化 main ;简化 main 的底层原理;当一个包内有多个 main 时加载顺序。

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

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

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

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

© 2021 V2EX