在 Java 的演進過程中,模式匹配一直是項重要的特性,但在處理基礎型別(primitive types)時卻存在許多限制。JDK 23 中的 JEP 455 為 Java 帶來了基礎型別模式匹配(pattern matching)的支援,讓開發人員能夠更優雅地處理型別轉換和檢查。
它不僅擴展了 instanceof
運算子的功能,也增強了 switch
的能力去處理所有基礎型別。這些改進大幅提升了 Java 程式碼的表達能力和安全性,同時也減少了冗長且容易出錯的程式碼。
目錄
前言
在傳統的 Java 開發中,處理基礎型別的轉換常常需要寫出冗長的程式碼。例如,當我們需要將一個 int
轉型為 byte
時,必須先進行數值範圍檢查,然後再執行明確的轉型操作。這不僅增加了程式碼的複雜度,還容易產生錯誤。
更重要的是,在處理基礎型別轉換時,可能會發生無聲的資料遺失,但現有的 Java 語法並沒有提供優雅的方式來處理這種情況。另外,switch
陳述式的限制也一直是開發人員的痛點。在此之前,switch
只能處理有限的幾種基礎型別,無法處理 boolean
、float
、double
或 long
型別,這限制了其應用場景。
為什麼需要基礎型別的模式匹配?
第一個限制是 switch
的模式匹配(JEP 441)不支援基礎型別的模式,僅支援指定參考類型(reference type)的模式匹配,例如 case Integer i
或 case String s
(自 Java 21 起,也支援記錄模式 JEP 440 用於 switch
)。
另一個限制是記錄(record
)的模式匹配對基礎型別的支援有限。當記錄類別中的某個欄位是基礎型別時,其模式匹配必須精確說明該值的型別。這對於開發人員來說是不方便的,並且與 Java 語言中存在的自動轉型不一致。例如,假設我們希望表示 JSON 數據:
sealed interface JsonValue {
record JsonString(String s) implements JsonValue { }
record JsonNumber(double d) implements JsonValue { }
record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}
JSON 不區分整數和非整數,因此 JsonNumber
使用 double
表示數字以獲得最大的靈活性。但是,我們在創建 JsonNumber
記錄時不需要傳遞 double
;我們可以傳遞一個 int
,例如 30,Java 編譯器會自動將 int
擴展為 double
:
var json = new JsonObject(Map.of("name", new JsonString("John"),
"age", new JsonNumber(30)));
遺憾的是,如果我們想用記錄模式分解 JsonNumber
,Java 編譯器就沒那麼好說話了。由於 JsonNumber
是用 double
宣告的,我們必須將 JsonNumber
分解為 double
,然後手動指定轉型為 int
:
if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(double a)) {
int age = (int) a; // 無法避免的(並且可能是有損的!)指定轉型
}
換句話說,基礎型別的模式匹配可以用在記錄類別上,但是其模式匹配裡的基礎型別必須與記錄欄位的基礎型別相同。無法通過 instanceof JsonNumber(int age)
來分解 JsonNumber
並讓編譯器自動將 double
窄化為 int
。
這項限制是因為窄化轉型有可能會損失訊息,意即 double
的值可能太大或精度太高,導入無法安全地窄化成為 int
變數。然而,模式匹配的其中一個好處是它可以通過不匹配來拒絕非法值。也就是說,如果 JsonNumber
的 double
無法安全地窄化成為 int
,那麼 instanceof JsonNumber(int age)
可以返回 false
,讓程式在不同的條件分支中處理大型 double
數值即可。
JEP 455 概觀
本功能解除了基礎型別在模式匹配中存在的幾個限制。處理基礎型別和物件型別的程式碼將會被簡化,不再需要特殊處理,使得這兩種型別可以用相同的方式應對。由於現在可以自動處理條件式轉型(數值必須被檢查以確保它對目標型別有效),因此允許 instanceof
接受基礎型別的話可以進一步減少程式碼數量。
它增強了 Java 的模式匹配功能,並擴展 instanceof
和 switch
,使其能夠在所有模式匹配的上下文中使用基礎型別:
- 基礎型別模式的增強:允許在模式匹配中適用更廣泛的候選基礎型別,例如
record Test(double d)
可以使用x instanceof Test(int i)
去判定d
值是否能轉型成int
instanceof
的擴展:允許instanceof
運算符與基礎型別一起使用,並且可以安全轉換,例如if (x instanceof int i) { ... }
switch
的擴展:允許switch
表達式處理所有基礎型別,包括boolean
、float
、double
和long
,例如case byte b -> ...
優點
- 使模式匹配在處理基礎型別和參考型別時更加一致,減少了開發人員需要記住的特殊情況,並且實現統一的資料處理方式
- 通過在
instanceof
和switch
中使用基礎型別模式,可避免不必要的裝箱和拆箱操作,從而提高程式碼的效能,並消除不安全轉型而導致資料損失的風險 - 擴展了
instanceof
和switch
的功能,使開發人員能夠編寫更具表達力和可讀性的程式碼
缺點
- 對於不熟悉模式匹配的開發人員來說,引入基礎型別模式可能會增加複雜度和學習曲線
- 雖然在某些情況下可以避免裝箱和拆箱操作,但在其他情況下,使用基礎型別模式可能會導致額外的效能影響
功能說明
讓我們來看看這項新功能如何改善程式碼。
switch
陳述句
透過在 switch
中支援基礎型別模式,我們可以改進 switch
表達式:
// 舊方式
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
default -> "unknown status: " + x.getStatus();
}
通過將 default
子句修改成為具有基礎型別模式的 case 子句,該模式公開匹配的值:
// 新方式
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
支援基礎型別模式還允許 guard 檢查匹配的值:
switch (x.getYearlyFlights()) {
case 1 -> normal();
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i -> issueSilverCard(); // 剩下的條件:when i > 2 && i < 100 ...
}
instanceof
陳述句
原本將 int
值轉型為 byte
是通過顯式的指定轉型執行的,但轉型有可能會損失訊息,因此必須在轉型前先進行繁瑣的範圍檢查:
// 舊方式
int value = 42;
if (value >= -128 && value <= 127) {
byte b = (byte) value;
// 使用 b
}
instanceof
中的基礎型別模式匹配會檢查該轉型是否會損失訊息,以避免開發人員近三十年來一直手動編寫的繁瑣範圍檢查。換句話說,instanceof
可以檢查數值和型別。上面的例子可以重寫如下:
// 新方式
if (value instanceof byte b) { // 如果可以安全轉型的話即為 true
// 使用 b,確保沒有資料遺失
}
結語
JEP 455 增強在模式匹配、instanceof
和 switch
中使用基礎型別,為 Java 開發人員帶來了更優雅、更安全的程式設計方式。透過統一的語法來處理型別檢查和轉型,不僅提高了程式碼的可讀性,也降低了發生錯誤的風險。
隨著這項功能的引入,Java 在型別系統的處理上更加完整和一致。雖然這可能需要一些時間來適應新的語法,但這項改進無疑將為 Java 開發帶來更好的開發體驗和更高的程式碼品質。
本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!