Unnamed Patterns and Variables in Java 21 – Java 中的未命名模式和变量 21

最后修改: 2023年 10月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

The release of Java 21 SE introduces an exciting preview feature: unnamed patterns and variables (JEP 443). This new addition allows us to reduce boilerplate code when side effects are our only concern.

Java 21 SE 的发布引入了一个令人兴奋的预览功能:未命名模式和变量 (JEP 443)。当我们只关心副作用时,这一新功能允许我们减少模板代码

Unnamed patterns are an improvement over Record Patterns In Java 19 and Pattern Matching for Switch. We should also be familiar with the Record functionality introduced as a preview in Java 14.

未命名模式是 Record Patterns In Java 19Pattern Matching for Switch 的改进。我们还应该熟悉在 Java 14 中作为预览版引入的 Record 功能。

In this tutorial, we’ll delve into how we can use these new features to improve our code quality and readability.

在本教程中,我们将深入探讨如何使用这些新功能来提高代码质量和可读性。

2. Purpose

2.目的

Usually, when working with complex objects, we don’t need all the data they hold at all times. Ideally, we receive from an object only what we need, but that’s rarely the case. Most of the time, we end up using one small part of what we’ve been given.

通常情况下,在处理复杂对象时,我们并不需要在任何时候都使用对象所保存的所有数据。理想情况下,我们只从对象中获得我们需要的内容,但这种情况很少发生。大多数情况下,我们最终使用的只是所给数据的一小部分。

Examples of this can be found all over OOP, evidentiated by the Single Responsibility Principle. The unnamed patterns and variable feature is Java’s latest attempt at tackling that on a lower scale.

这种例子在 OOP 中随处可见,单一责任原则就是明证。未命名模式和变量功能是 Java 在较小范围内解决这一问题的最新尝试。

Since this is a preview feature, we must ensure we enable it. In Maven, this is done by modifying the compiler plugin configuration to include the following compiler argument:

由于这是一项预览功能,我们必须确保启用它。在 Maven 中,这可以通过修改编译器插件配置来实现,其中包括以下编译器参数:

<compilerArgs>
    <arg>--enable-preview</arg>
</compilerArgs>

3. Unnamed Variables

3.未命名变量

While new to Java, this feature is well-loved in other languages such as Python and Go. Since Go isn’t precisely object-oriented, Java takes the lead in introducing this feature in the world of OOP.

虽然这项功能是 Java 的新功能,但在 Python 和 Go 等其他语言中却广受欢迎。由于 Go 并非完全面向对象,因此 Java 率先在 OOP 世界中引入了这一功能。

Unnamed variables are used when we care only about an operation’s side effects. They can be defined as many times as needed, but they cannot be referenced from a later point.

当我们只关心操作的副作用时,就会使用未命名变量。它们可以根据需要多次定义,但不能在以后引用。

3.1. Enhanced For Loop

3.1.增强型 For 循环

For starters, let’s say we have a simple Car record:

首先,假设我们有一个简单的 Car 记录:

public record Car(String name) {}

We then need to iterate through a collection of cars to count all cars and do some other business logic:

然后,我们需要遍历汽车集合,对所有汽车进行计数,并执行一些其他业务逻辑:

for (var car : cars) {
    total++;
    if (total > limit) {
        // side effect
    }
}

While we need to go through every element in the car collection, we don’t need to use it. Naming the variable makes the code harder to read, so let’s try the new feature:

虽然我们需要查看汽车集合中的每个元素,但我们并不需要使用它。命名变量会使代码更难读,所以让我们试试这个新功能:

for (var _ : cars) {
    total++;
    if (total > limit) {
        // side effect
    }
}

This makes it clear to the maintainer that the car isn’t used. Of course, this can also be used with a basic for loop:

这样,维护者就可以清楚地知道这辆车没有被使用。当然,这也可以用于基本的 for 循环:

for (int i = 0, _ = sendOneTimeNotification(); i < cars.size(); i++) {
    // Notify car
}

Note, however, that the sendOneTimeNotification() gets called only once. The method must also return the same type as the first initialization (in our case, i).

请注意,sendOneTimeNotification() 只被调用一次。该方法还必须返回与第一次初始化相同的类型(在我们的例子中为 i)。

3.2. Assignment Statements

3.2.赋值语句

We can also use unnamed variables with assignment statements. This is most useful when we need both a function’s side effects and some return values (but not all).

我们还可以在赋值语句中使用未命名变量。当我们同时需要函数的副作用和某些返回值(但不是全部)时,这种方法最有用

Let’s say we need a method that removes the first three elements in a queue and returns the first one:

比方说,我们需要一个删除队列中前三个元素并返回第一个元素的方法:

static Car removeThreeCarsAndReturnFirstRemoved(Queue<Car> cars) {
    var car = cars.poll();
    var _ = cars.poll();
    var _ = cars.poll();
    return car;
}

As we can see in the example above, we can use multiple assignments in the same block. We can also ignore the result of the poll() calls, but this way, it’s more readable.

正如我们在上面的示例中看到的,我们可以在同一个代码块中使用多个赋值。我们也可以忽略poll()调用的结果,但这样更易于阅读。

3.3. Try-Catch Block

3.3.Try-Catch 块

Possibly, the most helpful functionality of unnamed variables comes in the form of unnamed catch blocks. Many times, we want to handle exceptions without actually needing to know what the exception contains.

未命名变量最有用的功能可能就是未命名的捕获块。很多时候,我们想要处理异常,而实际上并不需要知道异常包含什么内容

With unnamed variables, we no longer have to worry about that:

有了未命名变量,我们就不用再担心这个问题了:

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!");
}

They also work for multiple exception types in the same catch:

它们也适用于同一捕获中的多种异常类型:

catch (IllegalStateException | NumberFormatException _) { }

3.4. Try-With Resources

3.4.试用资源

While encountered less than try-catch, the try-with syntax also benefits from this. For example, when working with databases, we don’t usually need the transaction object.

虽然遇到的情况比 try-catch 少,但 try-with 语法也从中受益。例如,在使用数据库时,我们通常不需要事务对象。

To take a better look at this, let’s first create a mock transaction:

为了更好地了解这一点,让我们先创建一个模拟交易:

class Transaction implements AutoCloseable {

    @Override
    public void close() {
        System.out.println("Closed!");
    }
}

Let’s see how this works:

让我们看看它是如何工作的:

static void obtainTransactionAndUpdateCar(Car car) {
    try (var _ = new Transaction()) {
        updateCar(car);
    }
}

And, of course, with multiple assignments:

当然,还有多项任务:

try (var _ = new Transaction(); var _ = new FileInputStream("/some/file"))

3.5. Lambda Parameters

3.5.Lambda 参数

Lambda functions offer, by nature, a great way of reutilizing code. It is only natural that, by offering this flexibility, we end up having to address cases that don’t interest us.

从本质上讲,Lambda 函数提供了重新利用代码的绝佳方式。通过提供这种灵活性,我们最终不得不处理我们不感兴趣的情况,这也是很自然的。

A great example of this is the computeIfAbsent() method from the Map interface. It checks if a value exists in the map or computes a new one based on a function.

Map 接口中的 computeIfAbsent() 方法就是一个很好的例子。该方法会检查地图中是否存在某个值,或根据某个函数计算出一个新值。

While useful, we usually don’t need the lambda’s parameter. It’s the same as the key passed to the initial method:

虽然很有用,但我们通常并不需要 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;
}

This works with multiple lambdas and multiple lambda parameters:

这适用于多个 lambdas 和多个 lambda 参数:

map.forEach((_, _) -> System.out.println("Works!"));

4. Unnamed Patterns

4. 未命名模式

Unnamed patterns have been introduced as an enhancement to Record Pattern Matching. They address an issue quite apparent: we usually don’t need every field in records we deconstruct.

记录模式匹配的增强功能引入了未命名模式。它们解决了一个非常明显的问题:我们通常不需要解构记录中的每个字段

To explore this topic, let’s first add a class called Engine:

为了探讨这一主题,让我们首先添加一个名为 Engine 的类:

abstract class Engine { }

The engine can be gas-based, electric, or hybrid:

发动机可以是燃气发动机、电动发动机或混合动力发动机:

class GasEngine extends Engine { }
class ElectricEngine extends Engine { }
class HybridEngine extends Engine { }

Finally, let’s extend the Car to support parameterized types so we can reuse it depending on the engine type. We’ll also add a new field called color:

最后,让我们扩展 Car 以支持 参数化类型,这样我们就可以根据引擎类型重用它。我们还将添加一个名为 color 的新字段:

public record Car<T extends Engine>(String name, String color, T engine) { }

4.1. instanceof

4.1. 实例

When deconstructing records with patterns, unnamed patterns enable us to ignore fields we don’t need.

使用模式解构记录时,未命名模式可让我们忽略不需要的字段。

Let’s say we get an object, and if it’s a car, we want to get its color:

假设我们得到了一个对象,如果它是一辆汽车,我们就想得到它的颜色:

static String getObjectsColor(Object object) {
    if (object instanceof Car(String name, String color, Engine engine)) {
        return color;
    }
    return "No color!";
}

While this works, it’s hard to read, and we’re defining variables we don’t need. Let’s see how this looks with unnamed patterns:

这样做虽然可行,但很难阅读,而且我们定义的变量并不需要。让我们看看使用未命名模式的效果如何:

static String getObjectsColorWithUnnamedPattern(Object object) {
    if (object instanceof Car(_, String color, _)) {
        return color;
    }
    return "No color!";
}

Now it’s clear we just need a car’s color.

现在很清楚了,我们只需要汽车的颜色

This also works for simple instanceof definitions, but it’s not quite as useful:

这也适用于简单的 instanceof 定义,但作用不大:

if (car instanceof Car<?> _) { }

4.2. Switch Patterns

4.2.开关模式

Deconstructing with switch patterns also allows us to ignore fields:

使用开关模式解构还允许我们忽略字段:

static String getObjectsColorWithSwitchAndUnnamedPattern(Object object) {
    return switch (object) {
        case Car(_, String color, _) -> color;
        default -> "No color!";
    };
}

Additionally to this, we can also handle parameterized cases. For example, we can handle different engine types in different switch cases:

除此之外,我们还可以处理参数化情况。例如,我们可以在不同的切换情况下处理不同的引擎类型:

return switch (car) {
    case Car(_, _, GasEngine _) -> "gas";
    case Car(_, _, ElectricEngine _) -> "electric";
    case Car(_, _, HybridEngine _) -> "hybrid";
    default -> "none";
};

We can also pair cases together more easily and also with guards:

我们还可以更方便地将案件配对在一起,同时也可以使用防护装置:

return switch (car) {
    case Car(_, _, GasEngine _), Car(_, _, ElectricEngine _) when someVariable == someValue -> "not hybrid";
    case Car(_, _, HybridEngine _) -> "hybrid";
    default -> "none";
};

5. Conclusions

5.结论

Unnamed patterns and variables are a great addition that addresses the Single Responsibility Principle. It’s a breaking change for versions before Java 8, but later versions aren’t affected since naming variables _ is not permitted.

未命名模式和变量是解决 “单一责任原则”(Single Responsibility Principle)的一大新增功能。对于 Java 8 之前的版本来说,这是一个突破性的变更,但由于不允许命名变量 _,因此以后的版本不会受到影响。

The feature kicks it out of the park by reducing boilerplate code and improving readability while making everything seem simpler.

该功能减少了模板代码,提高了可读性,同时使一切看起来更简单

As always, the code can be found over on GitHub.

一如既往,您可以在 GitHub 上找到代码。