1. Overview

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.

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.

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

2. Purpose

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.

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:

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

3. Unnamed Variables

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.

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

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

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 (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).

3.2. Assignment Statements

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.

3.3. Try-Catch Block

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

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.

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

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.

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.

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

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:

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

4. Unnamed Patterns

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:

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:

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

4.1. instanceof

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:

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

4.2. Switch Patterns

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

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.

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.