Looking at Java 22: Unnamed Variables & Patterns

 · 6 min
AI-generated by DALL-E

In this article, I want to talk about a non-preview feature belonging to Project Amber, JEP 456.

With Java 9, the _ (underscore) became a reserved keyword and no longer a valid identifier. Now, with the release of Java 22, the new keyword finally gets a specific meaning: something not having a name.


Everything Needs a Name

There are many scenarios where we declare a variable or are forced to do so without actually needing it. Be it variables in for-each-loops, in try-with-resources blocks, or lambda parameters, we always need to declare names, even if we might only be interested in a side-effect:

java
// SIDE-EFFECT ONLY
static int count(Iterable<Order> orders) {
  int total = 0;
  for (Order unused : orders) {
    total++;
  }
  return total;
}


// TRY-WITH-RESOURCES
try (var unused = ScopedContext.acquire()) {
  // ...
}


// TRY-CATCH
try {
  // ...
}
catch (Exception unused) {
  // ...
}


// LAMBDA PARAMETERS
...stream.collect(Collectors.toMap(String::toUpperCase,
                                   unused -> "NODATA"));


// ... and more

With underscore being a keyword since Java 9, I often abused $ (dollar) as a short substitute for unused variable names, especially for lambdas or catch blocks:

java
// UNUSED LAMBDA PARAMETER
Predicate<Account> activeFilter =
    activeOnly ? Account::isActive
               : $ -> Boolean.TRUE;


// UNUSED EXCEPTION
try {
  // ...
}
catch (Exception $) {
  // ...
}

While it’s technically legal to use the dollar sign, convention dictates it shouldn’t be used at all. The Java Language Specification (JLS §3.8) even says:

The dollar sign should be used only in mechanically generated source code or, rarely, to access pre-existing names on legacy systems.

Thankfully, JEP 456 brings in the officially sanctioned alternative!


Names No Longer Required

By making underscore a keyword, it can now be used as a substitute for any occurrences of an unnamed variable or pattern, even in the same context.

Unnamed Variables

First, let’s look at unnamed variables.

The following kinds of declarations support replacing a variable name with an underscore:

The previous examples look something like this now:

java
// SIDE-EFFECT ONLY
static int count(Iterable<Order> orders) {
  int total = 0;
  for (Order _ : orders) {
    total++;
  }
  return total;
}


// TRY-WITH-RESOURCES
try (var _ = ScopedContext.acquire()) {
  // ...
}


// TRY-CATCH
try {
  // ...
}
catch (Exception _) {
  // ...
}


// LAMBDA PARAMETERS
...stream.collect(Collectors.toMap(String::toUpperCase,
                                   _ -> "NODATA"));

Unlike in other languages, the underscore replaces only the variable/parameter name. The exception to this rule, though, is lambda parameters, as they do not necessarily need an explicit type in the first place.

Unnamed instanceof Record Patterns

Record patterns are a handy tool to destructure them:

java
enum Color { RED, GREEN, BLUE }

record Point(int x, int y) { /* ... */ }
record ColoredPoint(Point p, Color c) { /* ... */ }


if (r instanceof ColoredPoint(Point(int x, int y), Color c)) {
    // ...
}

However, redeclaring the whole Record for instanceof is quite cumbersome, especially for nested Records if we don’t need access to all of their components. With Java 22, an underscore can replace any component, even with or without its type:

java
if (r instanceof ColoredPoint(Point(int _, int y), _)) {
    // ...
}

The Record itself is not replaceable, as the compiler still needs to deduct which underlying Record is needed to be destructured:

java
// NOT ALLOWED ───────────────┐
//                            v
if (r instanceof ColoredPoint(_(int x, int y), _)) {
    // ...
}

// NOT ALLOWED ──┐
//               v
if (r instanceof _(Point(int x, int y), _)) {
  // ...
}

Unnamed switch case Patterns

Using underscore for conditional patterns follows the same rules as instanceof.

Let’s use sealed classes for a more straightforward example:

java
sealed interface HotDrink permits Coffee, Mocha, Cappucino, Latte { /* ... */ }

final class Coffee implements HotDrink { /* ... */ }
final class Mocha implements HotDrink { /* ... */ }
final class Cappucino implements HotDrink { /* ... */ }
final class Latte implements HotDrink { /* ... */ }

record Cup<T extends HotDrink>(T drink) { /* ... */ }

Now, we can create a switch statement working with the different types:

java
switch (cup) {
    case Cup(Cappucino _) -> addCookieToPlate();
    case Cup(Latte _)     -> addCookieToPlate();
    case Cup(Coffee _)    -> addMilk(cup);
    case Cup(var _)       -> serve();
}

With unnamed patterns, it’s likely that several case clauses will do the same thing for different patterns. That’s why the switch statements itself got changed recently, too!

JLS §14.11.1 changed as follows:

// JLS Java SE 20
SwitchLabel:
    case CaseConstant {, CaseConstant}
    default

// JLS Javca SE 21
SwitchLabel:
    case CaseConstant {, CaseConstant}
    case null [, default]
    case CasePattern {, CasePattern } [Guard]
    default

This simple change coming from Java 21’s introduction of switch patterns simplifies overlapping unnamed patterns:

java
switch (cup) {
    case Cup(Cappucino _),
         Cup(Latte _)  -> addCookieToPlate();
    case Cup(Coffee _) -> addMilk(cup);
    case Cup(var _)    -> serve();
}

Conclusion

Finally, having a keyword for unnamed variables and patterns instead of needing to fake them is a great addition to the language. And in good ol’ Java fashion, the change wasn’t hastily introduced, even though its journey started in Java 8 with the introduction of lambdas.

The need for unnamed parameters became more evident in the new lambda syntax, and the general idea of them was discussed in the “Lambda Leftovers” JEP 302. As a sole underscore was a valid identifier so far, the OpenJDK team made its usage a warning in Java 8, to not break any code.

Starting with Java 9, this warning became an error, freeing up the underscore until its rebirth as a keyword, and finally, using that keyword for unnamed variables and patterns.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources

Looking at Java 22