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 获取。


原始标题:Unnamed Patterns and Variables in Java 21