Java 22 introduced a tiny piece of syntax that fixes a surprisingly common annoyance: the underscore (_) as a stand-in for a variable or pattern you don't care about. Before this feature, every binding in a pattern needed a name, every catch clause needed a variable, and every lambda parameter needed an identifier, even when you were never going to read them. You'd invent names like unused, ignored, or tmp, and your IDE would still light up with "unused variable" warnings. This lesson covers where _ is allowed, where it isn't, and why being explicit about "I don't care" is a real readability win in e-commerce code that deconstructs orders, customers, and discounts.
When pattern matching for switch arrived in Java 21, deconstructing a record looked like this:
The pattern Discount(int percent, String code, String description) names all three components, but the description variable is never used inside the arrow body. We had to name it anyway, because the pattern needed a binding for every record component. A linter would flag description as unused. A reader skimming the code would wonder if there's a bug somewhere that forgot to reference it.
Unnamed variables solve this. From Java 22 onward, you can write String _ in place of any binding you don't need.
Same behavior, clearer intent. The reader sees String _ and immediately knows: "this slot is intentionally ignored." No more "did they forget to use description?" question.
Unnamed variables and patterns were a preview feature in Java 21 (JEP 443), a second preview in Java 21 (JEP 456), and finalized in Java 22. To use them in Java 21, you need to compile and run with --enable-preview --release 21. From Java 22 onward, they work out of the box with no flags. Everything in this lesson assumes Java 22 or later.
The most common place you'll use _ is inside a record deconstruction pattern. Each component of the record still needs a slot, but you can skip naming the ones you don't need.
The pattern Order(long _, Customer _, BigDecimal total) says: "destructure an Order, ignore the id, ignore the customer, bind the total." If you tried to write this without unnamed patterns, you'd end up with Order(long unusedId, Customer unusedCustomer, BigDecimal total), which is noisier and lies to the reader about whether those names will be referenced.
A regular variable name can appear only once in a scope. The compiler treats two int total declarations as a duplicate. Unnamed bindings are different: you can have as many _ slots as you want, because _ isn't a name at all. It's a hole.
Three _ bindings in one pattern, no conflict. This is the kind of code you write when you have a record with many fields but you only care about one or two for a particular branch.
Sometimes you want to ignore an entire component without even checking its type. Inside a record deconstruction, you can write var _ to bind nothing and skip the type check on that slot.
var _ is the most permissive form: it accepts any value at that position without checking the type. Customer _ is stricter; it would refuse to match if the component were somehow not a Customer. In a normal record where the component type is fixed, the distinction rarely matters, but for nested generic types or sealed hierarchies, var _ can save you a few keystrokes.
The diagram shows what happens when you destructure an Order. The pattern still requires three slots, but only the third one produces a usable variable. The first two are matched and discarded.
Pattern matching was the original motivation, but the Java 22 update extended _ to a handful of other places where unused names show up. Each one removes a small annoyance.
When you only need a loop count and not the elements, you used to write something like for (Product unused : products). With unnamed variables, the intent is explicit.
Of course, in real code you'd usually just write cart.size(). The example is contrived to show the syntax. A better case is when you're iterating to drive a side effect that doesn't need the element value, like calling a counter or emitting a log line for every item without inspecting it.
If you catch an exception only to convert it to a fallback value, the exception object itself is dead weight in the source.
catch (NumberFormatException _) reads as "I know this can throw, I don't need the details, just take the fallback path." That's a small but real improvement over catch (NumberFormatException e) followed by a body that never references e.
A word of caution: swallowing an exception's details with _ is fine when the catch arm logically converts the failure to a documented fallback. It's a smell when you're hiding a bug. Use the same judgment you'd use with catch (Exception e) { /* nothing */ }. The underscore is a tool, not a license to ignore errors.
When you open a resource purely for its side effect (acquiring a lock, holding a connection during a block, starting a timer), you might never reference the resource variable inside the block. try (var _ = ...) captures that intent.
The lock is acquired on entry to the try block and released when the block exits, exactly like any try-with-resources. We never call methods on the lock from inside the block, so naming it would be noise. var _ makes the "I only care about acquire/release" intent obvious.
try (var _ = ...) has no runtime cost compared to a named variable. The compiler still calls close() at the end of the block in both cases. The only difference is in the source, where _ signals "no body usage" to the reader.
Lambdas often receive parameters they don't need. A BiFunction that ignores its second argument used to require an unused name; now you can write _.
Map.merge calls the remapping function with the existing value and the new value. Here we always want "existing plus one" and never use the new value, so the second lambda parameter is _. The first call increments the existing Hoodie count from 1 to 2. The second call inserts Sticker with the initial value 1 (the merge function only fires when the key is already present, so _ isn't exercised in that path; for an absent key, merge just uses the second argument as-is).
When you call a method purely for its side effect but want to make "I'm intentionally ignoring the return value" explicit, you can assign it to _.
List.remove(Object) returns a boolean indicating whether the element was present. If you don't care about the return value but want to silence "ignored return" warnings or be explicit to readers, var _ = ... is one option. In practice this form is the least useful of the bunch, because calling cart.remove("Mug"); without capturing the return value already works. The var _ = ... form is mostly useful when a tool (a linter, a code review bot) flags discarded return values and you want a syntactic marker that says "yes, on purpose."
The flip side of the feature is just as important: there are places where _ is rejected. The rule of thumb is that _ is allowed only where the name wouldn't be readable anyway. If something else in the program might want to refer to the binding, _ doesn't work.
The diagram captures the rule. _ is allowed in contexts where the binding is local and unreferenced. It's rejected where the name has to be visible to other code.
A field named _ is illegal. The compiler refuses to define a field that nothing in the class can ever name.
This produces a compile error like:
Fix: Pick an actual field name. _ exists for places where the binding can't be read; a field is the opposite of that, because instance code reads fields all the time.
The same restriction applies to method names, class names, and top-level local variables that aren't part of one of the allowed patterns above. The compiler treats _ as a reserved identifier; it has been reserved since Java 9 to give the language room to grow into exactly this feature.
A regular method parameter named _ is also rejected. The reasoning: callers may pass arguments positionally, but the binding is still visible to the method body, and a method body that uses _ would have no way to refer to its own parameter. If you really don't need a parameter, you can name it anything you like (ignored, unused, etc.) without using _.
The compiler rejects this with the same "_ is a keyword" message. The fix is to give the parameter a real name, even if the body never references it.
None of this has runtime cost. _ is a source-level convenience. The bytecode generated for case Order(long _, Customer _, BigDecimal total) is the same as for a pattern with named (but unused) bindings, minus the named local variable slots that the compiler would otherwise allocate. The savings are in readability, not performance.
Here's a more realistic e-commerce example that uses unnamed patterns and variables in a few places at once. We'll process a list of recent orders, extract just the totals for orders that shipped, and ignore everything else about each record.
The first switch arm uses three _ bindings to ignore the id, the customer, and (in the second arm) the total. The single s binding picks up the status so the when guard can check it. The second arm acts as a catch-all that returns zero. The reader can see at a glance that id and customer are deliberately not part of the computation.
This is the kind of code where unnamed patterns are most useful. Without _, every record component would need a fresh, unused name in every arm, and the actual logic ("look at the status, return the total if shipped") would be buried under boilerplate.
A few rules of thumb from early community usage:
var _ = method(); because a static analyzer complains about a discarded return value, ask whether you should be using the return value. Discarded boolean returns from remove or add are sometimes signs of a bug.catch (NumberFormatException _) { return 0; } is fine because the fallback is documented. catch (IOException _) { } with an empty body is the same anti-pattern it always was; the _ just makes the silence louder.10 quizzes