Contents

Using _ (single underscore) as a variable name (Java 22+) signals intentional non-use to both the compiler and future readers. The compiler suppresses the "unused variable" warning that would otherwise appear. This is useful wherever syntax requires a named variable but the value is irrelevant — loop counters used only to repeat a body a fixed number of times, loop variables in enhanced-for when only side effects matter, catch parameters when the exception type is sufficient context, and lambda parameters required by the functional interface signature but not needed in the body. Multiple _ declarations in the same scope are allowed because they are not treated as the same variable.

// 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 } Before Java 22, _ (single underscore) was a valid (if discouraged) identifier. Since Java 9 it produced a warning; since Java 22 it is reserved as the unnamed variable/pattern syntax and can no longer be used as an identifier.

In instanceof and switch type patterns, _ matches any value of the given type without binding it. This is useful when only the type matters and the value is not needed in the case body. It is also the correct idiom for a catch-all type pattern in a switch: case SomeType _ handles that type without introducing an unused binding. Combine with when guards that access the original selector expression rather than the binding.

// 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"); }

In record deconstruction patterns, _ skips individual components you do not need. For example, Point(int x, _) matches any Point and binds only x, without creating a binding for y. This is cleaner than binding a variable you will never reference and avoids compiler warnings. In nested patterns, _ can skip entire nested records, keeping the pattern focused on only the components that matter to the logic.

// 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"; }; }

In switch expressions, case _ -> using pattern syntax acts as the default match-any case. When combined with a sealed type, the unnamed pattern can cover remaining subtypes without listing them individually — for instance, after handling the specific subtypes you care about, a trailing case _ -> handles anything else. Unnamed type patterns in switch also allow grouping: case Integer _, Long _, Short _ -> applies the same arm to multiple types without needing a separate binding for each.

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"; }; }

catch (Exception _) silences the "unused exception variable" warning when you genuinely do not need the exception object — for example, when logging a fixed message or rethrowing a different exception. In lambdas, (_, b) -> b * 2 signals that the first parameter of a two-parameter functional interface is intentionally ignored, making the intent clear without resorting to a conventional dummy name like ignored or unused. This applies wherever a multi-parameter functional interface is required but only some parameters are used.

// 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])); Unnamed patterns and variables are a readability feature. Use them whenever you're required to name something (by syntax) but the value is genuinely unused. It signals to readers: "There's a value here by design, but this code deliberately ignores it."