Java Unnamed Patterns and Variables: _ in Pattern Matching (Java 22+)
Unnamed patterns and variables, finalized in Java 22 (JEP 456), allow you to use an underscore _ wherever a variable is required but its value is not needed. This prevents the compiler error of unused variables, makes intent explicit ('I know there's a value here but I don't need it'), and produces cleaner switch arms and pattern matches. It works in type patterns, record patterns, catch clauses, lambda parameters, and enhanced for loops.
Contents
Unnamed Variables
Using __
// Java 22+ (JEP 456) — finalized
// Before Java 22: have to name a variable you don't use
try {
int result = compute();
} catch (Exception ex) { // "ex" is unused — some IDEs warn
System.out.println("Computation failed");
}
// Java 22: use _ instead
try {
int result = compute();
} catch (Exception _) { // clear: we know there's an exception but don't need it
System.out.println("Computation failed");
}
// For loops — counting without needing the loop variable
for (int _ = 0; _ < 5; _++) {
System.out.println("Tick");
}
// Enhanced for — iterating for side effects only
for (String _ : list) {
count++; // we only care about count, not the element
}
// Multiple _ are allowed — each _ is independent
int _ = getFirst();
int _ = getSecond(); // valid — not a redeclaration of the same name
// Assignment statement — compute for side effects, discard result
_ = methodWithSideEffect(); // explicit discard
// Try-with-resources — open stream for side effect, don't need the reference
try (var _ = acquireResource()) {
doWork(); // resource auto-closed, but we never use it by name
}
_Unnamed Type Patterns
In instanceofswitch_case SomeType _when
// Type patterns in instanceof — unnamed when you only care about the type
Object obj = getObject();
// Before — must name the binding even if unused
if (obj instanceof String s) {
System.out.println("It's a String");
// s is never used — compiler warning / linting issue
}
// Java 22 — unnamed type pattern
if (obj instanceof String _) {
System.out.println("It's a String"); // no binding needed
}
// Useful in switch when multiple arms do the same thing regardless of binding
switch (obj) {
case Integer _ -> System.out.println("int");
case Double _ -> System.out.println("floating");
case String _ -> System.out.println("text");
default -> System.out.println("other");
}
// Nested type checks without caring about the binding value
static boolean isNumeric(Object o) {
return o instanceof Number _; // true if Number, no binding needed
}
// Combining with guards — use _ for the binding, guard on the type
switch (shape) {
case Circle _ when getArea(shape) > 100 -> System.out.println("Big circle");
case Circle _ -> System.out.println("Small circle");
case Rectangle _ when getArea(shape) > 100 -> System.out.println("Big rectangle");
default -> System.out.println("Other");
}
Unnamed Record Patterns
In record deconstruction patterns, _Point(int x, _)Pointxy_
// Records can be destructured in patterns — unnamed _ for unneeded components
record Point(int x, int y) {}
record Line(Point start, Point end) {}
record ColoredPoint(Point p, Color c) {}
Object obj = new ColoredPoint(new Point(1, 2), Color.RED);
// Named binding — old way, even when components are unused
if (obj instanceof ColoredPoint(Point p, Color c)) {
System.out.println(p); // c is unused
}
// Unnamed for unused components
if (obj instanceof ColoredPoint(Point p, Color _)) {
System.out.println(p); // we only care about the Point
}
// Fully unnamed record deconstruction — just checking the structure
if (obj instanceof ColoredPoint(Point _, Color _)) {
System.out.println("It's a ColoredPoint");
}
// Nested record patterns with selective unnamed
if (obj instanceof ColoredPoint(Point(int x, int _), Color _)) {
System.out.println("x coordinate: " + x); // only care about x
}
// In switch
String describe(Object o) {
return switch (o) {
case ColoredPoint(Point(int x, int y), Color _) ->
"Point at " + x + "," + y; // don't need color
case Line(Point(int x, int _), Point _) ->
"Line starting at x=" + x; // only start.x matters
default -> "unknown";
};
}
Unnamed in Switch Expressions
In switch expressions, case _ ->case _ ->case Integer _, Long _, Short _ ->
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double w, double h) implements Shape {}
record Triangle(double base, double height) implements Shape {}
// Before — must name binding even when only using the type for routing
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.w() * r.h();
case Triangle t -> 0.5 * t.base() * t.height();
};
}
// Java 22 — use _ when record component extracted separately
double area(Shape shape) {
return switch (shape) {
case Circle(var r) -> Math.PI * r * r;
case Rectangle(var w, var h)-> w * h;
case Triangle(var b, var h) -> 0.5 * b * h;
};
}
// Cleaner: multiple null-or-type cases with unnamed
String classify(Object obj) {
return switch (obj) {
case null -> "null";
case Integer i when i < 0 -> "negative int: " + i;
case Integer _ -> "non-negative int";
case String s -> "string: " + s;
case Number _ -> "other number";
default -> "something else";
};
}
// Grouping arms — same action for multiple types
String describe(Object obj) {
return switch (obj) {
case Integer _, Long _, Short _, Byte _ -> "integral type";
case Float _, Double _ -> "floating point";
case String _ -> "text";
default -> "other";
};
}
Unnamed in catch and Lambdas
catch (Exception _)(_, b) -> b * 2ignoredunused
// catch clauses — very common use case
try {
process();
} catch (IOException _) {
log("IO error occurred"); // don't need the exception details
} catch (ParseException _) {
log("Parse error");
}
// Multi-catch with unnamed
try {
riskyOperation();
} catch (IllegalArgumentException | IllegalStateException _) {
System.out.println("Invalid operation");
}
// Lambda parameters — when you must implement a functional interface with N params
// but you only care about some of them
BiConsumer<String, Integer> printKey = (key, _) -> System.out.println(key);
BiFunction<String, Integer, String> makeLabel = (_, count) -> "Count: " + count;
// Map.forEach — care about key but not value
Map<String, List<Integer>> data = Map.of("a", List.of(1, 2), "b", List.of(3));
data.forEach((key, _) -> System.out.println("Key: " + key));
// Event listeners with multiple params
button.addActionListener(_ -> handleClick()); // don't need the ActionEvent
// Comparator — comparing only by first element
List<int[]> intervals = List.of(new int[]{1,3}, new int[]{0,4}, new int[]{2,5});
intervals.sort((a, _) -> a[0]); // wrong! just example of syntax — sort by a[0] only
// Actually: intervals.sort(Comparator.comparingInt(a -> a[0]));