Java 21 中的未命名模式與變數
1. 概述
Java 21 SE 的發布引入了一項令人興奮的預覽功能:未命名模式和變數 ( JEP 443 )。當副作用是我們唯一關心的問題時,這個新增功能使我們能夠減少樣板程式碼。
未命名模式是 Java 19 中的記錄模式和 Switch 模式匹配的改進。我們也應該熟悉 Java 14 中作為預覽引入的記錄功能。
在本教程中,我們將深入研究如何使用這些新功能來提高程式碼品質和可讀性。
2. 目的
通常,在處理複雜物件時,我們不需要它們始終保存的所有資料。理想情況下,我們只從物件接收我們需要的東西,但這種情況很少發生。大多數時候,我們最終都會使用我們所得到的一小部分。
這樣的例子在 OOP 中隨處可見,單一職責原則證明了這一點。未命名的模式和變數功能是 Java 在較小規模上解決這個問題的最新嘗試。
由於這是預覽功能,我們必須確保啟用它。在 Maven 中,這是透過修改編譯器插件配置以包含以下編譯器參數來完成的:
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
3. 未命名變數
雖然對 Java 來說是個新功能,但此功能在 Python 和 Go 等其他語言中很受歡迎。由於 Go 並不完全是物件導向的,因此 Java 在 OOP 領域率先引入了這項特性。
當我們只關心操作的副作用時,使用未命名變數。可以根據需要多次定義它們,但以後不能引用它們。
3.1.增強型 For 循環
首先,假設我們有一個簡單的Car
記錄:
public record Car(String name) {}
然後,我們需要迭代cars
集合來計算所有汽車的數量並執行一些其他業務邏輯:
for (var car : cars) {
total++;
if (total > limit) {
// side effect
}
}
雖然我們需要檢查汽車集合中的每個元素,但我們不需要使用它。命名變數會使程式碼更難閱讀,所以讓我們嘗試一下新功能:
for (var _ : cars) {
total++;
if (total > limit) {
// side effect
}
}
這讓維修人員清楚知道這輛車沒有被使用過。當然,這也可以與基本的 for 迴圈一起使用:
for (int i = 0, _ = sendOneTimeNotification(); i < cars.size(); i++) {
// Notify car
}
但請注意, sendOneTimeNotification()
只被呼叫一次。該方法還必須傳回與第一次初始化相同的類型(在我們的例子中為i
)。
3.2.賦值語句
我們也可以將未命名變數與賦值語句一起使用。當我們既需要函數的副作用又需要一些回傳值(但不是全部)時,這是最有用的。
假設我們需要一個方法來刪除佇列中的前三個元素並傳回第一個:
static Car removeThreeCarsAndReturnFirstRemoved(Queue<Car> cars) {
var car = cars.poll();
var _ = cars.poll();
var _ = cars.poll();
return car;
}
正如我們在上面的範例中看到的,我們可以在同一個區塊中使用多個賦值。我們也可以忽略poll()
呼叫的結果,但這樣,它的可讀性更強。
3.3. Try-Catch 區塊
未命名變數最有用的功能可能以未命名 catch 區塊的形式出現。很多時候,我們想要處理異常,但其實不需要知道異常包含什麼。
有了未命名的變量,我們就不用再擔心了:
try {
someOperationThatFails(car);
} catch (IllegalStateException _) {
System.out.println("Got an illegal state exception for: " + car.name());
} catch (RuntimeException _) {
System.out.println("Got a runtime exception!");
}
它們也適用於同一捕獲中的多種異常類型:
catch (IllegalStateException | NumberFormatException _) { }
3.4.嘗試資源
雖然遇到的情況比try-catch
少,但try-with
語法也從中受益。例如,在使用資料庫時,我們通常不需要事務物件。
為了更好地了解這一點,我們首先創建一個模擬交易:
class Transaction implements AutoCloseable {
@Override
public void close() {
System.out.println("Closed!");
}
}
讓我們看看這是如何運作的:
static void obtainTransactionAndUpdateCar(Car car) {
try (var _ = new Transaction()) {
updateCar(car);
}
}
當然,還有多項作業:
try (var _ = new Transaction(); var _ = new FileInputStream("/some/file"))
3.5.拉姆達參數
從本質上講,Lambda 函數提供了一種重複使用程式碼的好方法。很自然,透過提供這種靈活性,我們最終不得不解決我們不感興趣的案例。
一個很好的例子是Map
介面中的computeIfAbsent()
方法。它檢查映射中是否存在值或根據函數計算新值。
雖然很有用,但我們通常不需要 lambda 參數。它與傳遞給初始方法的金鑰相同:
static Map<String, List<Car>> getCarsByFirstLetter(List<Car> cars) {
Map<String, List<Car>> carMap = new HashMap<>();
cars.forEach(car ->
carMap.computeIfAbsent(car.name().substring(0, 1), _ -> new ArrayList<>()).add(car)
);
return carMap;
}
這適用於多個 lambda 和多個 lambda 參數:
map.forEach((_, _) -> System.out.println("Works!"));
4. 未命名模式
引入未命名模式作為記錄模式匹配的增強。他們解決了一個非常明顯的問題:我們通常不需要解構records
中的每個欄位。
為了探討這個主題,我們先加入一個名為Engine
的類別:
abstract class Engine { }
發動機可以是燃氣發動機、電動發動機或混合動力發動機:
class GasEngine extends Engine { }
class ElectricEngine extends Engine { }
class HybridEngine extends Engine { }
最後,讓我們擴展Car
以支援參數化類型,以便我們可以根據引擎類型重複使用它。我們還將新增一個名為color
的新欄位:
public record Car<T extends Engine>(String name, String color, T engine) { }
4.1. instanceof
當使用模式解構記錄時,未命名模式使我們能夠忽略不需要的欄位。
假設我們得到一個物體,如果它是一輛汽車,我們想要得到它的顏色:
static String getObjectsColor(Object object) {
if (object instanceof Car(String name, String color, Engine engine)) {
return color;
}
return "No color!";
}
雖然這有效,但很難閱讀,而且我們正在定義不需要的變數。讓我們看看未命名模式的外觀如何:
static String getObjectsColorWithUnnamedPattern(Object object) {
if (object instanceof Car(_, String color, _)) {
return color;
}
return "No color!";
}
現在很明顯我們只需要汽車的color
。
這也適用於簡單的instanceof
定義,但不太有用:
if (car instanceof Car<?> _) { }
4.2.切換模式
使用 switch 模式解構也允許我們忽略欄位:
static String getObjectsColorWithSwitchAndUnnamedPattern(Object object) {
return switch (object) {
case Car(_, String color, _) -> color;
default -> "No color!";
};
}
除此之外,我們還可以處理參數化的情況。例如,我們可以在不同的switch
情況下處理不同的engine
類型:
return switch (car) {
case Car(_, _, GasEngine _) -> "gas";
case Car(_, _, ElectricEngine _) -> "electric";
case Car(_, _, HybridEngine _) -> "hybrid";
default -> "none";
};
我們也可以更輕鬆地將箱子與防護裝置配對:
return switch (car) {
case Car(_, _, GasEngine _), Car(_, _, ElectricEngine _) when someVariable == someValue -> "not hybrid";
case Car(_, _, HybridEngine _) -> "hybrid";
default -> "none";
};
5。結論
未命名的模式和變數是解決單一職責原則的一個很好的補充。對於 Java 8 之前的版本來說,這是一個重大更改,但更高版本不受影響,因為不允許命名變數_
。
該功能透過減少樣板程式碼和提高可讀性,同時使一切看起來更簡單,將它踢出了公園。
與往常一樣,可以在 GitHub 上找到程式碼。