1. 概述
Java 21 SE 引入了一项激动人心的预览特性:未命名模式与变量(JEP 443)。这项新特性允许我们在只关心副作用时减少样板代码。
未命名模式是对 Java 19 的记录模式 和 Switch 模式匹配 的改进。同时需要熟悉 Java 14 作为预览特性引入的 Record 功能。
本教程将深入探讨如何利用这些新特性提升代码质量和可读性。
2. 目的
通常处理复杂对象时,我们并不总是需要其包含的所有数据。理想情况下,我们只从对象中获取所需部分,但实际情况往往并非如此。大多数时候,我们最终只使用了所获数据的一小部分。
这种场景在面向对象编程中随处可见,单一职责原则 就是明证。未命名模式与变量特性是 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) {
// 副作用操作
}
}
虽然需要遍历每个元素,但实际并未使用 car
。命名变量反而降低可读性,试试新特性:
for (var _ : cars) {
total++;
if (total > limit) {
// 副作用操作
}
}
这明确告诉维护者 car
未被使用。当然,也可用于基础 for 循环:
for (int i = 0, _ = sendOneTimeNotification(); i < cars.size(); i++) {
// 通知车辆
}
注意: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 块处理多个异常类型:
catch (IllegalStateException | NumberFormatException _) { }
3.4. Try-With Resources
虽然不如 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 参数
Lambda 函数天然提供了代码复用的绝佳方式。这种灵活性自然导致我们需要处理不关心的参数。
Map
接口的 computeIfAbsent()
方法就是典型例子。它检查映射中是否存在值,或基于函数计算新值。
虽然实用,但通常不需要 lambda 的参数——它与初始方法传入的 key 相同:
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 和多个参数:
map.forEach((_, _) -> System.out.println("Works!"));
4. 未命名模式
未命名模式作为 记录模式匹配 的增强被引入。它解决了一个明显问题:解构记录时通常不需要所有字段。
为探讨此主题,先添加一个 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 模式
使用 switch 模式解构时同样可以忽略字段:
static String getObjectsColorWithSwitchAndUnnamedPattern(Object object) {
return switch (object) {
case Car(_, String color, _) -> color;
default -> "No color!";
};
}
此外还能处理参数化情况。例如在不同 switch 分支处理不同引擎类型:
return switch (car) {
case Car(_, _, GasEngine _) -> "gas";
case Car(_, _, ElectricEngine _) -> "electric";
case Car(_, _, HybridEngine _) -> "hybrid";
default -> "none";
};
还能更轻松地组合分支并使用守卫(guards):
return switch (car) {
case Car(_, _, GasEngine _), Car(_, _, ElectricEngine _) when someVariable == someValue -> "not hybrid";
case Car(_, _, HybridEngine _) -> "hybrid";
default -> "none";
};
5. 结论
未命名模式与变量是针对单一职责原则的绝佳补充。这对 Java 8 之前的版本是破坏性变更,但后续版本不受影响(因为命名变量为 _
本就不被允许)。
该特性通过减少样板代码、提升可读性并简化逻辑,表现堪称完美。
代码示例可在 GitHub 获取。