Java 22 introduces Unnamed Variables & Patterns, a feature designed to reduce clutter and improve code readability by allowing developers to explicitly mark variables and pattern components as unused. This is particularly useful in scenarios where you're forced to declare variables due to syntax requirements but don't actually need to use them.
What are Unnamed Variables & Patterns?
Unnamed variables are denoted by the underscore character _ and indicate that a variable is declared but intentionally not used. Unnamed patterns extend this concept to pattern matching, allowing you to ignore components in deconstruction patterns.
Key Benefits
- Improved Readability: Clearly signals intentional non-use of variables
- Reduced Clutter: Eliminates unused variable warnings and unnecessary names
- Better Maintenance: Makes code intent clearer for other developers
- Compiler Enforcement: Prevents accidental use of unnamed variables
Basic Unnamed Variables Usage
Example 1: Basic Unnamed Variables
public class UnnamedVariablesBasic { public static void main(String[] args) { // Traditional approach - unused variable causes warning try { int result = Integer.parseInt("123"); System.out.println("Parsed successfully"); // 'result' is declared but not used - IDE warning } catch (NumberFormatException e) { System.out.println("Parse failed"); } // Java 22 - using unnamed variable try { int _ = Integer.parseInt("123"); // No warning - explicitly unused System.out.println("Parsed successfully"); } catch (NumberFormatException e) { System.out.println("Parse failed"); } // Multiple unnamed variables int _ = calculateValue(); String _ = fetchData(); System.out.println("Operations completed"); } static int calculateValue() { return 42; } static String fetchData() { return "data"; } } Example 2: Enhanced For-Loops
import java.util.List; public class UnnamedVariablesLoops { public static void main(String[] args) { List<String> items = List.of("apple", "banana", "cherry"); // Traditional - counter variable not used int counter = 0; for (String item : items) { System.out.println("Processing..."); // counter++; // Forgot to increment - bug! } // Java 22 - explicitly ignore loop variable when only count matters int count = 0; for (String _ : items) { count++; } System.out.println("Total items: " + count); // Multiple loops with unnamed variables processMultipleCollections(); } static void processMultipleCollections() { List<List<String>> nestedLists = List.of( List.of("a", "b", "c"), List.of("x", "y", "z") ); // We only care about the number of inner lists, not their contents for (List<String> _ : nestedLists) { performOperation(); } } static void performOperation() { System.out.println("Operation performed"); } } Unnamed Variables in Real-World Scenarios
Example 3: Exception Handling
import java.util.Optional; public class UnnamedVariablesExceptions { public static void main(String[] args) { // Traditional exception handling with unused variable try { riskyOperation(); } catch (UnsupportedOperationException ex) { // 'ex' is declared but not used System.out.println("Operation not supported"); } // Java 22 - explicitly ignore exception details try { riskyOperation(); } catch (UnsupportedOperationException _) { System.out.println("Operation not supported"); } // Multiple catch blocks with unnamed variables try { anotherRiskyOperation(); } catch (IllegalArgumentException _) { System.out.println("Invalid argument"); } catch (UnsupportedOperationException _) { System.out.println("Operation not supported"); } // Optional handling Optional<String> optionalValue = Optional.empty(); if (optionalValue.isEmpty()) { String _ = optionalValue.orElse("default"); // Explicitly ignore System.out.println("Using default value"); } } static void riskyOperation() { throw new UnsupportedOperationException("Not implemented"); } static void anotherRiskyOperation() { // Simulate different exceptions if (Math.random() > 0.5) { throw new IllegalArgumentException("Bad argument"); } else { throw new UnsupportedOperationException("Not supported"); } } } Example 4: Collections and Streams
import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; public class UnnamedVariablesCollections { public static void main(String[] args) { Map<String, Integer> scores = Map.of( "Alice", 95, "Bob", 87, "Charlie", 92 ); // Traditional - entry variable not fully used scores.forEach((name, score) -> { System.out.println("Processing student..."); // Only using the action, not the key/value }); // Java 22 - explicitly ignore both key and value scores.forEach((_, _) -> { System.out.println("Processing student record"); }); // Stream operations with unused elements List<String> names = List.of("Alice", "Bob", "Charlie"); long count = names.stream() .map(_ -> "Processed") // Ignore input element .count(); System.out.println("Processed " + count + " items"); // More complex stream example processWithSideEffects(names); } static void processWithSideEffects(List<String> items) { // We want to execute a side effect for each item but don't need the item itself items.stream() .map(_ -> { performSideEffect(); return null; }) .collect(Collectors.toList()); } static void performSideEffect() { System.out.println("Side effect executed"); } } Unnamed Patterns in Pattern Matching
Example 5: Record Patterns with Unnamed Components
public class UnnamedPatterns { // Sample records record Point(int x, int y, String label) {} record Rectangle(Point topLeft, Point bottomRight) {} record Person(String name, int age, String email) {} public static void main(String[] args) { Point point = new Point(10, 20, "origin"); Rectangle rect = new Rectangle(new Point(0, 0, "tl"), new Point(100, 100, "br")); Person person = new Person("John", 30, "[email protected]"); // Traditional record pattern - all components must be named if (point instanceof Point(int x, int y, String label)) { System.out.println("Point at (" + x + ", " + y + ")"); // 'label' is unused but we're forced to declare it } // Java 22 - unnamed pattern components if (point instanceof Point(int x, int y, String _)) { System.out.println("Point at (" + x + ", " + y + ")"); // No unused variable warning } // Nested patterns with unnamed components if (rect instanceof Rectangle(Point(int x1, int y1, String _), Point(int x2, int y2, String _))) { int width = x2 - x1; int height = y2 - y1; System.out.println("Rectangle size: " + width + "x" + height); } // Switch expressions with unnamed patterns String result = switch (person) { case Person(String name, int _, String _) when name.length() > 3 -> "Long name"; case Person(String _, int age, String _) when age > 25 -> "Adult"; case Person(String _, int _, String _) -> "Other"; }; System.out.println("Person category: " + result); } // Method using unnamed patterns static String extractName(Person person) { return switch (person) { case Person(String name, int _, String _) -> name; }; } static int calculateArea(Rectangle rect) { if (rect instanceof Rectangle(Point(int x1, int y1, String _), Point(int x2, int y2, String _))) { return Math.abs(x2 - x1) * Math.abs(y2 - y1); } return 0; } } Example 6: Advanced Pattern Matching Scenarios
import java.util.*; public class AdvancedUnnamedPatterns { // Complex nested records record Address(String street, String city, String zip) {} record Customer(String id, String name, Address address, List<Order> orders) {} record Order(String orderId, double amount, Date orderDate) {} public static void main(String[] args) { Customer customer = new Customer( "C123", "Alice", new Address("123 Main St", "Springfield", "12345"), List.of( new Order("O1", 99.99, new Date()), new Order("O2", 149.99, new Date()) ) ); // Complex pattern with multiple unnamed components if (customer instanceof Customer(String id, String name, Address(String _, String city, String _), List<Order> _)) { System.out.println("Customer " + name + " from " + city + " (ID: " + id + ")"); } // Processing collections with unnamed patterns processCustomers(List.of(customer)); // Switch with nested unnamed patterns analyzeCustomer(customer); } static void processCustomers(List<Customer> customers) { for (Customer customer : customers) { // We only care about customer ID and order count, not order details if (customer instanceof Customer(String id, String _, Address _, List<Order> orders)) { System.out.println("Customer " + id + " has " + orders.size() + " orders"); } } } static void analyzeCustomer(Customer customer) { String analysis = switch (customer) { case Customer(String id, String name, Address(_, String city, _), List<Order> orders) when orders.size() > 5 -> "Frequent buyer from " + city; case Customer(String _, String _, Address(_, String city, _), List<Order> _) when city.equals("New York") -> "NYC customer"; case Customer(String _, String name, Address _, List<Order> _) when name.startsWith("A") -> "A-name customer"; case Customer(String _, String _, Address _, List<Order> _) -> "Regular customer"; }; System.out.println("Analysis: " + analysis); } // Method with multiple unnamed parameters in lambda static void initializeSystem() { List<Runnable> initializers = List.of( () -> System.out.println("Initializing module A"), () -> System.out.println("Initializing module B"), () -> System.out.println("Initializing module C") ); // We don't care about the index, just need to run each initializer for (int i = 0; i < initializers.size(); i++) { Runnable _ = initializers.get(i); // Explicitly ignore initializers.get(i).run(); } } } Best Practices and Guidelines
Example 7: Proper Usage Patterns
import java.util.concurrent.*; public class UnnamedVariablesBestPractices { public static void main(String[] args) throws Exception { // ✅ GOOD: Clear intent with unnamed variables goodPractices(); // ❌ BAD: Common pitfalls to avoid badPractices(); // Mixed scenarios mixedUsage(); } static void goodPractices() { // 1. Exception handling where exception details aren't needed try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException _) { // We don't need the exception details, just handling the case Thread.currentThread().interrupt(); } // 2. Loop counters when only presence matters List<String> items = List.of("a", "b", "c"); for (String _ : items) { performBackgroundTask(); } // 3. Method returns when we only care about side effects String _ = System.setProperty("key", "value"); // 4. Pattern matching when only some components are needed record Pair(String key, String value) {} Pair pair = new Pair("name", "John"); if (pair instanceof Pair(String key, String _)) { System.out.println("Key: " + key); } } static void badPractices() { // ❌ Don't use unnamed variables when the value is actually needed List<String> names = List.of("Alice", "Bob"); // BAD: We actually need the names later // for (String _ : names) { // System.out.println("Processing..."); // } // System.out.println("First name: " + names.get(0)); // This would fail! // GOOD: Use meaningful names when values are needed for (String name : names) { System.out.println("Processing: " + name); } // ❌ Don't overuse in simple cases where names are clear int total = 10; // int _ = total; // Overkill - just use the meaningful name // ❌ Avoid in public API documentation examples // Public examples should be clear and educational } static void mixedUsage() { // Complex scenario with mixed named and unnamed variables ExecutorService executor = Executors.newFixedThreadPool(2); Future<String> future1 = executor.submit(() -> "Result1"); Future<String> future2 = executor.submit(() -> "Result2"); try { // We only care if both complete successfully, not their individual results String _ = future1.get(1, TimeUnit.SECONDS); String _ = future2.get(1, TimeUnit.SECONDS); System.out.println("Both tasks completed successfully"); } catch (TimeoutException _) { System.out.println("Tasks timed out"); } catch (ExecutionException _) { System.out.println("Task execution failed"); } catch (InterruptedException _) { Thread.currentThread().interrupt(); } finally { executor.shutdown(); } } static void performBackgroundTask() { // Simulate background work try { Thread.sleep(50); } catch (InterruptedException _) { Thread.currentThread().interrupt(); } } } Migration and Compatibility
Example 8: Gradual Adoption Strategy
public class UnnamedVariablesMigration { // Existing code with unused variables public static void oldStyleCode() { // Before: Unused variables causing warnings List<String> data = List.of("a", "b", "c"); @SuppressWarnings("unused") int unusedCounter = 0; // Had to suppress warnings for (String unusedItem : data) { System.out.println("Processing..."); } try { int unusedResult = riskyCall(); System.out.println("Call succeeded"); } catch (Exception unusedEx) { System.out.println("Call failed"); } } // Modernized code with unnamed variables public static void newStyleCode() { // After: Clean, intentional code List<String> data = List.of("a", "b", "c"); int _ = 0; // No warning needed for (String _ : data) { System.out.println("Processing..."); } try { int _ = riskyCall(); System.out.println("Call succeeded"); } catch (Exception _) { System.out.println("Call failed"); } } static int riskyCall() { return 42; } // Mixed approach during migration public static void gradualMigration() { // Keep meaningful names where they add value List<String> importantData = List.of("critical", "data", "points"); for (String item : importantData) { processImportantItem(item); // We actually use the item } // Use unnamed for trivial cases List<String> logEntries = readLogEntries(); for (String _ : logEntries) { incrementCounter(); // Don't care about log content, just count } } static void processImportantItem(String item) { System.out.println("Processing: " + item); } static List<String> readLogEntries() { return List.of("log1", "log2", "log3"); } static void incrementCounter() { // Counter implementation } } Key Rules and Limitations
- Single Usage: Only one unnamed variable
_can be used in a scope - No Reuse: Cannot reference unnamed variables after declaration
- Compiler Enforcement: Attempting to use
_after declaration causes compile-time error - Not a Keyword:
_is not a reserved keyword, but has special meaning in variable declarations
Conclusion
Java 22's Unnamed Variables & Patterns provide significant benefits for writing cleaner, more intentional code:
- ✅ Clear Intent: Explicitly marks unused variables as intentional
- ✅ Reduced Noise: Eliminates unnecessary variable names and warnings
- ✅ Better Patterns: Enhances pattern matching with selective component ignoring
- ✅ Gradual Adoption: Can be adopted incrementally in existing codebases
This feature is particularly valuable in:
- Exception handling blocks
- Loop constructs where elements aren't used
- Pattern matching deconstructions
- API calls where return values are ignored
- Test code with setup/teardown operations
By adopting unnamed variables and patterns, Java developers can write more maintainable code that clearly communicates intent while reducing visual clutter and potential bugs from accidentally unused variables.