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.