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
| Release | Status | Spec |
|---|---|---|
| Java 21 | Preview | JEP 443 |
| Java 22 | Final | JEP 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:
| Form | Example | Meaning |
|---|---|---|
| Unnamed variable | var _ = audit() | Initialize but bind no usable name |
| Unnamed pattern variable | case Failed _ -> | Test a type but bind no variable |
| Unnamed pattern | Point(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-levelinstanceoforswitchpattern. - 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.