隨著 Java 程式語言的不斷演進,開發者們一直在尋求更簡潔且高效的程式碼編寫方式。因此 Java 23 中引入預覽功能 JEP 476 模組匯入聲明,可以簡化 Java 模組化程式中的模組 import
方式,並為開發者提供一種更便捷的方式來使用模組函式庫。
本文將探討模組匯入的核心概念、功能特性,並探討它在實際開發中的優勢和挑戰。無論您是經驗豐富的 Java 工程師,還是剛接觸模組化概念的新手,您都可以藉由本篇文章學習到此功能所帶來的好處。
目錄
前言
在前幾天介紹的 JEP 477 隱式宣告類別中有提到,Java 編譯器在建立隱式類別時,會自動添加模組匯入的語句:
import module java.base;
其實自動匯入型別的能力,在 Java 誕生時就已經存在了。大家可以回想一下,我們撰寫 Java 程式時都不需要明確指定匯入 java.lang
套件(package)中的類別和介面(例如 Object
、String
和 Comparable
)。Java 編譯器會自動加上隨選型別匯入(type-import-on-demand)的語句,去匯入 java.lang
套件中的所有類別和介面,就好像每個原始檔的開頭都出現了以下語句一樣:
import java.lang.*;
隨著 Java 的發展,List
、Map
、Stream
和 Path
等類別和介面也變得幾乎同樣重要。然而,它們都不在 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
聲明有兩種主要形式:
- 單一型別匯入 (single-type-import):這種形式明確地指定欲匯入的類別或介面。例如:
import java.util.List;
代表只匯入java.util
套件中的List
介面,其他在同一個套件中的類別或介面不會被匯入。 - 隨選型別匯入 (type-import-on-demand):這種形式使用星號來表示匯入指定套件中的所有類別和介面。例如:
import java.util.*;
代表會匯入java.util
套件中的所有類別和介面,例如List
、Map
、Set
等。
單一型別匯入因為會明確指定全部要匯入的類別或介面,因此有助於提高程式碼的可讀性並避免命名衝突。不過,它的代價則是匯入語句可能會高達數十甚至上百行,尤其是當引用的類別很多時,造成維護困難。隨選型別匯入不需條列所有類別,可以一次匯入指定套件中的所有類別和介面,方便快捷。但缺點是可能會導致命名衝突,尤其是在大型專案中。
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 模組依賴的其他模組中的套件
靈活的使用場景
在龐大、成熟的程式碼庫中,清晰度至關重要,因此許多人偏好單一型別匯入。然而,在早期階段,便利性勝過清晰度的情況下,開發人員通常更喜歡隨選型別匯入。它特別適用於以下情況:
- 建立小型程式或原型(prototype)驗證並使用 java 啟動器運行時
- 在 JShell 中探索新的 API 時,例如 Stream Gatherers(JEP 473 規格)或 Foreign Function & Memory API(JEP 454 規格)
- 在學習使用新的 API 和新功能進行程式設計時,例如虛擬執行緒及其執行器
- 與 JEP 477 隱式宣告類別一起合作,降低初學者的學習門檻
語法和語義
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 開發的效率和體驗。作為軟體工程師,保持對新技術的開放態度,同時謹慎評估其實際應用價值,將是在快速變化的技術環境中保持競爭力的關鍵。
本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!