JDK 23 功能:JEP 476 模組化的匯入聲明

JEP 476: Module Import Declarations

隨著 Java 程式語言的不斷演進,開發者們一直在尋求更簡潔且高效的程式碼編寫方式。因此 Java 23 中引入預覽功能 JEP 476 模組匯入聲明,可以簡化 Java 模組化程式中的模組 import 方式,並為開發者提供一種更便捷的方式來使用模組函式庫。

本文將探討模組匯入的核心概念、功能特性,並探討它在實際開發中的優勢和挑戰。無論您是經驗豐富的 Java 工程師,還是剛接觸模組化概念的新手,您都可以藉由本篇文章學習到此功能所帶來的好處。

前言

在前幾天介紹的 JEP 477 隱式宣告類別中有提到,Java 編譯器在建立隱式類別時,會自動添加模組匯入的語句:

import module java.base;

其實自動匯入型別的能力,在 Java 誕生時就已經存在了。大家可以回想一下,我們撰寫 Java 程式時都不需要明確指定匯入 java.lang 套件(package)中的類別和介面(例如 ObjectStringComparable)。Java 編譯器會自動加上隨選型別匯入(type-import-on-demand)的語句,去匯入 java.lang 套件中的所有類別和介面,就好像每個原始檔的開頭都出現了以下語句一樣:

import java.lang.*;

隨著 Java 的發展,ListMapStreamPath 等類別和介面也變得幾乎同樣重要。然而,它們都不在 java.lang 中,所以不會被自動匯入。相反地,開發人員必須通過在每個檔案的開頭寫入大量的 import 語句來讓編譯器滿意。例如,下面程式碼中的匯入語句幾乎與程式碼本體一樣多:

import java.util.Map;                   // 或 import java.util.*;
import java.util.function.Function;     // 或 import java.util.function.*;
import java.util.stream.Collectors;     // 或 import java.util.stream.*;
import java.util.stream.Stream;         // (使用上一行語句後可以移除)

String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m = 
        Stream.of(fruits)
              .collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1),
                                        Function.identity()));

對於使用單一型別匯入(single-type-import)或隨選型別匯入(type-import-on-demand)的宣告方式,不同的開發人員有著不同的看法。然而自 Java 9 以來,我們已使用模組將多組套件結合在一起,以便在單一名稱下使用。所以如果開發人員可以將整個模組(即從模組匯出的所有套件)隨選匯入的話就會很方便,能夠一次性全部匯入所有套件。

Single-type-import vs. type-import-on-demand

在 Java 中,import 聲明有兩種主要形式:

  1. 單一型別匯入 (single-type-import):這種形式明確地指定欲匯入的類別或介面。例如:
    import java.util.List;
    代表只匯入 java.util 套件中的 List 介面,其他在同一個套件中的類別或介面不會被匯入。
  2. 隨選型別匯入 (type-import-on-demand):這種形式使用星號來表示匯入指定套件中的所有類別和介面。例如:
    import java.util.*;
    代表會匯入 java.util 套件中的所有類別和介面,例如 ListMapSet 等。

單一型別匯入因為會明確指定全部要匯入的類別或介面,因此有助於提高程式碼的可讀性並避免命名衝突。不過,它的代價則是匯入語句可能會高達數十甚至上百行,尤其是當引用的類別很多時,造成維護困難。隨選型別匯入不需條列所有類別,可以一次匯入指定套件中的所有類別和介面,方便快捷。但缺點是可能會導致命名衝突,尤其是在大型專案中。

JEP 476 概觀

JEP 476 為 Java 模組化程式提供了模組匯入方式。它減少了樣板代碼、提高了程式碼的可維護性,並與其他模組化語言的匯入機制更加一致。儘管可能會影響可讀性並帶來潛在的命名衝突,但對於熟悉模組化概念的開發人員來說,這是一個不錯的功能。

優點

  • 簡化程式碼:大幅減少 import 語句,使程式碼簡潔易讀
  • 提高開發效率:減少處理個別套件、類別、介面匯入的時間
  • 增強可維護性:當模組中的套件結構發生變化時,無需修改大量匯入語句,從而提高了程式碼的可維護性。例如套件更名
  • 促進模組化思維:鼓勵開發者開始考慮模組級別的設計思維
  • 與其他模組化語言(如 Python 和 JavaScript)的模組匯入機制更加一致

缺點

  • 可能導致命名衝突:一次性匯入模組的所有套件可能會導致命名衝突,尤其是在大型專案中
  • 降低程式碼可讀性:對於不熟悉模組概念的開發人員來說,可能難以理解具體匯入了哪些內容,也降低了程式碼可讀性

介紹

模組匯入聲明的語法

JEP 476 的核心是引入了新的模組匯入 import module 語句,這允許開發人員一次匯入模組中導出的所有套件,減少了冗長的匯入語句:

import module java.base;
// 等同於導入 java.base 模組導出的所有套件
// 如 java.util.*, java.io.* 等

import static java.io.IO.*;

class ModuleImport {
    void main() { 
        var list = List.of("World!", "All!", ":)");
        var n = new Random().nextInt(list.size());
        println("Hello, " + list.get(n));
    }
}

自動匯入相依的模組套件

模組匯入聲明不僅僅只匯入在語句中明確指定的模組套件,它同時還會自動導入該模組所依賴的其他模組套件。這大大提高了程式碼的複用性和模組間的互操作性。

import module java.sql;
// 不僅導入 java.sql.* 和 javax.sql.*
// 還會導入 java.sql 模組依賴的其他模組中的套件

靈活的使用場景

在龐大、成熟的程式碼庫中,清晰度至關重要,因此許多人偏好單一型別匯入。然而,在早期階段,便利性勝過清晰度的情況下,開發人員通常更喜歡隨選型別匯入。它特別適用於以下情況:

語法和語義

import module 接受一個模組名稱,因此不可能從未命名模組(即從類別路徑)中匯入套件。這與模組檔案(即 module-info.java)中的 requires 子句一致,它需接受模組名稱因此不能表達對未命名模組的依賴。

有時匯入未指定任何套件的模組是有用的,因為該模組相依於其他有指定匯出套件的模組。例如,java.se 模組不匯出任何套件,但它相依於其他 19 個模組,因此 import module java.se 的效果是匯入這些相依模組中匯出的套件;也就是說,java.se 模組中間接匯出的 123 個套件。

請注意,在未命名模組的編譯單元中,例如隱式聲明類別的編譯單元中,不能使用 import module java.se

模糊匯入的衝突解決

當程式中匯入多個模組時,難免會產生匯入衝突的狀況,導致了模糊匯入。也就是說在匯入不同的模組的時候,因為其中有同名類別,導致程式引用時不知道該使用哪一個類別。例如下面幾個情況:

import module java.desktop; // 匯出 javax.swing.text,
                            // 其中有一個公共的 Element 介面,
                            // 還匯出 javax.swing.text.html.parser,
                            // 其中有一個公共的 Element 類別
...
Element e = ...; // 錯誤 - 不明確的名稱!
...

在此原始碼檔案中,簡單名稱 List 是不明確的:

import module java.base;    // 匯出 java.util,其中有一個公共的 List 介面
import module java.desktop; // 匯出 java.awt,其中有一個公共的 List 類別
...
List l = ...; // 錯誤 - 不明確的名稱!
...

最後一個例子,在此原始碼檔案中,簡單名稱 Date 是不明確的:

import module java.base; // 匯出 java.util,其中有一個公共的 Date 類別
import module java.sql;  // 匯出 java.sql,其中有一個公共的 Date 類別
...
Date d = ...; // 錯誤 - 不明確的名稱!
...

此時我們需要另外用型別匯入去指明。

import module java.base;  // java.util.Date
import module java.sql;   // java.sql.Date

import java.sql.Date;  // 需使用單一型別匯入去指明要用哪一個 Date

與隱式宣告類別合作

隱式類別會自動匯入 java.base 模組中所有公共頂層類別和介面,使得常用的 API 可以直接使用而無需額外的 import 語句。

// import module java.base; // 預設模組匯入
void main() {
    var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
    for (var name : authors) {
        println(name + ": " + name.length());
    }
}

總結

模組匯入語法的引入,標誌著 Java 在簡化開發流程和增強模組化支持方面邁出了重要一步。這項功能不僅簡化了程式碼結構,還促進了更佳的模組化設計實踐。對於追求效率和清晰度的開發團隊來說,這無疑是一個值得關注和嘗試的新特性。

然而,如同任何新技術一樣,JEP 476 的採用需要謹慎考慮。開發者應該權衡其帶來的便利性與潛在的風險,在實際專案中合理使用。隨著 Java 生態系統的不斷發展,我們期待看到更多創新特性的出現,進一步提升 Java 開發的效率和體驗。作為軟體工程師,保持對新技術的開放態度,同時謹慎評估其實際應用價值,將是在快速變化的技術環境中保持競爭力的關鍵。

本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

17 − 16 =

返回頂端