Java has always made you name some values even when the name adds nothing: an exception you will not inspect, a lambda parameter you will not read, or the unneeded half of a record pattern.

Developers worked around that with names such as ignored and unused. Those are still ordinary variables that somebody can accidentally use later.

Java 22 gives _ a precise meaning: this value has no usable name.

try {
    refreshCache();
} catch (CacheAlreadyFreshException _) {
    // The exception type matters; the object does not.
}

This is the final form of Unnamed Variables and Patterns, delivered by JEP 456. It first previewed in Java 21 and became permanent in Java 22 without changes.

The problem _ solves

Consider a map operation where the key matters but the input value does not:

Map<String, String> labels = names.stream()
        .collect(Collectors.toMap(
                String::toUpperCase,
                ignored -> "NODATA"));

ignored is in scope, and a later edit can read it even though this lambda was designed to return a constant. Java 22 lets the code state its intent directly:

Map<String, String> labels = names.stream()
        .collect(Collectors.toMap(
                String::toUpperCase,
                _ -> "NODATA"));

There is no variable named _ to read. The underscore occupies the declaration slot but introduces no name into scope.

Use _ for deliberate non-use. Keep a descriptive name when the value explains the code or the body needs it.

When it shipped

ReleaseStatusSpec
Java 21PreviewJEP 443
Java 22FinalJEP 456

No preview flag is required on Java 22 or newer.

Note: A single underscore stopped being a legal identifier in Java 9. Java 22 does not restore it as a normal name; it gives _ narrow unnamed-variable and unnamed-pattern roles. Names such as _value and __, plus numeric separators such as 1_000, are unchanged.

Mental model

Java 22 adds three closely related forms:

FormExampleMeaning
Unnamed variablevar _ = audit()Initialize but bind no usable name
Unnamed pattern variablecase Failed _ ->Test a type but bind no variable
Unnamed patternPoint(int x, _)Ignore a nested record component

The last two are different:

case Failed _ -> handleFailure(); // type pattern, unnamed variable
case Envelope(_) -> handleAny();  // bare unnamed pattern, nested

Failed _ checks a type. Bare _ is an unconditional pattern equivalent to var _, and it is allowed only inside a record pattern.

Unnamed local variables

An unnamed local is useful when evaluating the initializer matters but retaining its result does not:

while (coordinates.size() >= 3) {
    var x = coordinates.remove();
    var y = coordinates.remove();
    var _ = coordinates.remove(); // dequeue padding
    draw(new Point(x, y));
}

The third remove() still runs; only its result is discarded. An unnamed local requires an initializer, and multiple underscores can appear in one block because none declares a name:

var _ = warmPrimaryCache();
var _ = warmSecondaryCache();

String _;              // error: initializer required
System.out.println(_); // error: no variable named _

The initializer still executes. _ discards a result, not side effects.

Loop declarations

Use _ when the loop count matters but each element does not. It is also legal in a basic for initializer:

static int count(Iterable<Order> orders) {
    int total = 0;
    for (Order _ : orders) {
        total++;
    }
    return total;
}

for (int i = 0, _ = initializeBatch(); i < 10; i++) {
    process(i);
}

If the element helps explain the algorithm, keep a descriptive name.

Try-with-resources

Some resources establish a scope or lock but are never called from the body:

try (var _ = ScopedContext.open(requestId)) {
    handleRequest();
}

Initialization and automatic closing still happen. Unnamed resources require an initializer; use a normal name if the block invokes methods on the resource.

Unnamed catch parameters

A catch clause must declare a parameter even when recovery depends only on the exception type:

static int parsePort(String text) {
    try {
        return Integer.parseInt(text);
    } catch (NumberFormatException _) {
        return 8080;
    }
}

This works with multi-catch too:

try {
    loadConfiguration();
} catch (IOException | IllegalArgumentException _) {
    useDefaults();
}

Do not hide diagnostic context. Keep a named exception when logs, wrapping, metrics, or branching need its details.

Unnamed lambda parameters

Constant-producing lambdas often receive parameters only because the functional interface requires them:

Map<String, String> statuses = userIds.stream()
        .collect(Collectors.toMap(
                Function.identity(),
                _ -> "PENDING"));

users.replaceAll((_, user) -> user.withActive(true));

BiFunction<String, Integer, UUID> newId =
        (_, _) -> UUID.randomUUID();

The usual lambda typing rule still applies: parameters must be all inferred or all explicit.

BiFunction<String, String, String> keepFirst =
        (String first, String _) -> first;

Fields and method parameters are outside JEP 456:

class InvalidExamples {
    private String _;         // error: unnamed field
    void consume(String _) {} // error: unnamed method parameter
}

The feature targets declarations whose visibility stays inside a method: locals, exception parameters, and lambda parameters.

Unnamed pattern variables

Type patterns sometimes test the variant without needing the matched object:

sealed interface PaymentResult permits Paid, Declined, RetryLater {}

record Paid(String transactionId) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record RetryLater(Duration delay) implements PaymentResult {}

static boolean shouldNotify(PaymentResult result) {
    return switch (result) {
        case Paid _ -> true;
        case Declined _ -> true;
        case RetryLater _ -> false;
    };
}

Each label still performs a type test. It simply avoids variables that the right-hand side never reads.

This fits naturally with the exhaustive switches from Sealed Classes + Pattern Switch. The sealed hierarchy gives the compiler every variant; _ keeps cases focused when only the variant matters.

The same form works with instanceof:

if (result instanceof Declined _) {
    metrics.increment("payment.declined");
}

Declined _ is a top-level unnamed type pattern. It is legal because the type is present.

Unnamed patterns in nested records

Bare _ is most useful when record patterns are nested:

record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point point, Color color) {}

static int xCoordinate(ColoredPoint value) {
    if (value instanceof ColoredPoint(Point(int x, _), _)) {
        return x;
    }
    throw new IllegalArgumentException("point required");
}

The pattern extracts only x. The underscores say that y and color are structurally present but irrelevant. Java 22 removes both invented names and, for bare _, the unnecessary component type.

Keep the type when its test matters but its value does not:

if (value instanceof ColoredPoint(Point(int x, int _), Color _)) {
    return x;
}

Bare _ matches any component value, including null. An unnamed type pattern such as Color _ still performs its type test and therefore does not match null.

The top-level limit

Bare _ is not a top-level pattern. These forms do not compile:

if (value instanceof _) { } // error

switch (value) {
    case _ -> handle(value); // error
}

The unnamed pattern exists to omit a component inside a record pattern. At the top level, write the type:

if (value instanceof ColoredPoint _) {
    handleColoredPoint();
}

The easy rule is:

Type _       valid type pattern at top level or nested
_            valid unnamed pattern only inside a record pattern

Combining switch alternatives

JEP 456 also allows multiple patterns in one case when none declares a named pattern variable:

static String action(PaymentResult result) {
    return switch (result) {
        case Paid _, Declined _ -> "notify";
        case RetryLater _ -> "wait";
    };
}

A named pattern variable in any alternative is a compile-time error: there would be no single binding reliably available to the shared right-hand side.

Cheat sheet

Java 22 / JEP 456: final, no preview flag

Local:              var _ = sideEffect();
Enhanced for:       for (Item _ : items) { ... }
Basic for init:     for (int i = 0, _ = init(); ... ) { ... }
Resource:           try (var _ = openScope()) { ... }
Catch:              catch (IOException _) { ... }
Lambda:             _ -> constant
Type pattern:       case Failed _ -> ...
Nested record:      case Box(_) -> ...

Type _  = type test, no bound name; top-level or nested
_       = unconditional unnamed pattern; nested records only

Not supported: unnamed fields or method parameters
Unnamed locals/resources require an initializer
Multiple _ declarations are allowed; none can be referenced

Do:

  • Use _ to make deliberate non-use compiler-enforced.
  • Keep the type in Type _ when the type test matters.
  • Use bare _ for irrelevant nested record components.
  • Pair unnamed type patterns with exhaustive sealed-type switches.

Don’t:

  • Use bare _ as a top-level instanceof or switch pattern.
  • Replace every unused-variable warning without checking for dead logic.
  • Discard exceptions whose cause should be logged or propagated.
  • Expect _ to work for fields, method parameters, generics, or imports.
  • Pretend side effects disappear when a result is unnamed.

Wrap-up

Unnamed variables and patterns make absence explicit. In Java 22, _ means the program needs a declaration or pattern position, but the value does not deserve a usable name.

Use it for side-effect-only locals, loops, scoped resources, catch values, and lambda inputs. Use Type _ when a type test matters, and bare _ only inside record patterns.

Combined with sealed classes and pattern switch, this small feature keeps exhaustive domain handling compact without hiding which variants the compiler checks.