V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
HikariLan
V2EX  ›  分享创造

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

  •  
  •   HikariLan ·
    shaokeyibb · 2023-06-05 01:35:22 +08:00 · 5360 次点击
    这是一个创建于 572 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    引用

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

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

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

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

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

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

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