Part 11 of 15

Unnamed Patterns and Variables (JEP 443): Writing Intent-Clear Code

Preview Feature — Requires --enable-preview at compile and runtime. The _ identifier was reserved in Java 9; this JEP gives it formal semantics.

The Problem: Forced Naming of Unused Things

When pattern matching, you often care about only some components of a matched structure. But the syntax forces you to name everything:

record Point(int x, int y) {}
record ColoredPoint(Point point, String color) {}
record Box(ColoredPoint cp, int weight) {}

// You only care about the color — but must name everything else
if (obj instanceof Box(ColoredPoint(Point(int x, int y), String color), int weight)) {
    System.out.println("Color: " + color);
    // x, y, weight are declared but never used → IDE warnings
}

Similarly in catch blocks:

try {
    riskyOperation();
} catch (IOException e) {
    // e is never used — just logging a fixed message
    log.error("IO failed");
}

The variable e is a noise declaration: it exists only because the syntax requires it, not because you need it.


The Unnamed Pattern: _

In Java 21 with preview enabled, _ (single underscore) is the unnamed pattern — it matches any value and binds nothing:

// Only care about color — discard x, y, weight with _
if (obj instanceof Box(ColoredPoint(Point(_, _), String color), _)) {
    System.out.println("Color: " + color);
}

Multiple _ in the same pattern are independent — each discards one component. They don’t conflict with each other.


Unnamed Variables in catch Blocks

try {
    riskyOperation();
} catch (IOException _) {           // unnamed — won't use the exception
    log.error("IO operation failed");
}

// Multiple catches — all unnamed if not needed
try {
    parseAndProcess(input);
} catch (NumberFormatException _) {
    log.warn("Bad number format, using default");
    return DEFAULT_VALUE;
} catch (ParseException _) {
    log.warn("Parse failed, using default");
    return DEFAULT_VALUE;
}

Unnamed Variables in Enhanced for Loops

When iterating a collection purely for its side effects (count, accumulate externally):

int count = 0;
for (var _ : items) {
    count++;
}

// Or just to trigger side effects N times
for (var _ : requests) {
    processNextInQueue();
}

Unnamed Variables in Assignment

// Call a method for its side effects, discard the return value explicitly
var _ = map.put("key", value);  // signaling: return value intentionally ignored

// Common in multi-assignment scenarios
var (result, _) = twoValueReturn();  // hypothetical — shows intent

Full Pattern Matching Example with _

sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height, String color) implements Shape {}

// Only interested in area — ignore color for Triangle
static double area(Shape shape) {
    return switch (shape) {
        case Circle(double r)              -> Math.PI * r * r;
        case Rectangle(double w, double h) -> w * h;
        case Triangle(double b, double h, _) -> 0.5 * b * h;  // color discarded
    };
}

Combining with Type Patterns

_ works as an unnamed type pattern too — match any type without binding:

static boolean isInteresting(Object obj) {
    return switch (obj) {
        case String s when !s.isEmpty() -> true;  // named — we use s
        case Integer _                  -> true;  // unnamed — any Integer qualifies
        case _                          -> false; // unnamed default pattern
    };
}

case _ is the unnamed default — equivalent to default but participates in exhaustiveness checking for sealed types.


Scope and Reuse Rules

  • _ cannot be read — it has no name; System.out.println(_) is a compile error
  • Multiple _ in the same scope are independent — they don’t shadow each other
  • _ as an identifier (variable name with length ≥ 1) is still an error since Java 9: int _ = 5; is illegal; int _x = 5; is still legal
// Legal: multiple independent _ in one pattern
if (obj instanceof Box(ColoredPoint(Point(_, _), _), _)) { ... }

// Illegal: trying to read _
if (obj instanceof Box(ColoredPoint(Point(_, _), String color), _)) {
    System.out.println(_);  // COMPILE ERROR — _ has no name
}

Before and After

// BEFORE — noisy, forces naming of unused components
record OrderEvent(String orderId, String customerId, double amount,
                  String currency, Instant timestamp, String source) {}

if (event instanceof OrderEvent(String id, String cid, double amt,
                                String cur, Instant ts, String src)) {
    // only use id and amt — all other vars declared but unused
    processOrder(id, amt);
}

// AFTER — intent clear, compiler won't warn about unused vars
if (event instanceof OrderEvent(String id, _, double amt, _, _, _)) {
    processOrder(id, amt);
}

Key Takeaways

  • _ is the unnamed pattern — matches any value, binds nothing, creates no variable
  • Works in: record pattern components, type patterns, instanceof, switch, catch, and for-each
  • Multiple _ in one pattern are independent — each discards one position independently
  • _ cannot be read — it is a write-only discard, not a variable
  • The unnamed default pattern case _ participates in exhaustiveness checking unlike default
  • Requires --enable-preview in Java 21; stabilization expected in a future release

Next: Unnamed Classes and Instance Main Methods (JEP 445) — write Java programs without class declarations for scripts, teaching, and quick experiments.