Introduction to Pattern Matching in Java 21
Java 21 represents a significant milestone in pattern matching evolution, introducing powerful new features that make code more concise, readable, and less error-prone. This guide covers all pattern matching enhancements through Java 21.
Table of Contents
- Pattern Matching for
instanceof - Record Patterns
- Pattern Matching for
switch - Unnamed Patterns and Variables
- Guarded Patterns
- Practical Examples and Best Practices
1. Pattern Matching for instanceof (Finalized in Java 21)
Traditional Approach vs Pattern Matching
// Traditional approach - verbose and error-prone
public class InstanceOfTraditional {
public static void processObject(Object obj) {
if (obj instanceof String) {
String str = (String) obj; // Explicit casting
System.out.println("String length: " + str.length());
} else if (obj instanceof Integer) {
Integer num = (Integer) obj; // Explicit casting
System.out.println("Integer value: " + num);
}
}
}
// Java 21 Pattern Matching - concise and safe
public class InstanceOfPatternMatching {
public static void processObject(Object obj) {
if (obj instanceof String str) { // Pattern variable 'str' declared inline
System.out.println("String length: " + str.length()); // No casting needed
} else if (obj instanceof Integer num) {
System.out.println("Integer value: " + num); // Direct usage
}
}
}
Advanced instanceof Patterns
public class AdvancedInstanceOf {
// Pattern with additional conditions
public static void processWithConditions(Object obj) {
if (obj instanceof String str && str.length() > 5) {
System.out.println("Long string: " + str);
}
if (obj instanceof Integer num && num > 0) {
System.out.println("Positive integer: " + num);
}
}
// Nested patterns
public static void processNested(Object obj) {
if (obj instanceof List<?> list && !list.isEmpty()
&& list.get(0) instanceof String firstElement) {
System.out.println("First element: " + firstElement);
}
}
// Scope of pattern variables
public static void patternVariableScope(Object obj1, Object obj2) {
if (obj1 instanceof String s1 && obj2 instanceof String s2) {
// Both s1 and s2 are in scope here
System.out.println("Both are strings: " + s1 + ", " + s2);
}
// s1 and s2 are out of scope here - compile error if used
}
}
2. Record Patterns (Java 21 Preview → Final)
Basic Record Patterns
// Define records
record Point(int x, int y) {}
record Circle(Point center, double radius) {}
record Rectangle(Point topLeft, Point bottomRight) {}
public class RecordPatterns {
// Traditional approach with instanceof and manual decomposition
public static void processShapeTraditional(Object shape) {
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
Point center = circle.center();
int x = center.x();
int y = center.y();
System.out.println("Circle at (" + x + ", " + y + ")");
}
}
// Java 21 Record Patterns - nested decomposition
public static void processShapeModern(Object shape) {
if (shape instanceof Circle(Point(int x, int y), double radius)) {
System.out.println("Circle at (" + x + ", " + y + ") with radius " + radius);
}
}
// Partial decomposition
public static void processShapePartial(Object shape) {
if (shape instanceof Circle(Point center, double radius)) {
System.out.println("Circle with center and radius: " + radius);
// Can still access center as a whole
System.out.println("Center: " + center.x() + ", " + center.y());
}
}
}
Advanced Record Pattern Examples
// Complex record hierarchies
record Address(String street, String city, String zipCode) {}
record Person(String name, int age, Address address) {}
record Employee(String id, Person person, String department) {}
public class AdvancedRecordPatterns {
// Deeply nested patterns
public static void extractEmployeeInfo(Object obj) {
if (obj instanceof Employee(String id,
Person(String name, int age, Address(String street, String city, String zipCode)),
String department)) {
System.out.println(name + " works in " + department + " at " + street + ", " + city);
}
}
// Pattern matching in method parameters
public static void printPersonDetails(Person(String name, int age, Address address)) {
System.out.println(name + " is " + age + " years old");
}
// Combining with collections
public static void processPeople(List<Object> people) {
for (Object person : people) {
if (person instanceof Person(String name, int age, Address addr)) {
if (age > 18) {
System.out.println(name + " is an adult");
}
}
}
}
// Record patterns with generics
record Pair<T, U>(T first, U second) {}
public static void processPair(Object pair) {
if (pair instanceof Pair<String, Integer>(String name, Integer age)) {
System.out.println(name + " -> " + age);
}
if (pair instanceof Pair<?, ?> p) {
// Wildcard pattern
System.out.println("Pair: " + p.first() + ", " + p.second());
}
}
}
3. Pattern Matching for switch (Java 21 Final)
Basic Switch Pattern Matching
public class SwitchPatternMatching {
// Traditional switch statement
public static String traditionalSwitch(Object obj) {
if (obj instanceof String s) {
return "String: " + s;
} else if (obj instanceof Integer i && i > 0) {
return "Positive integer: " + i;
} else if (obj instanceof Integer i) {
return "Non-positive integer: " + i;
} else {
return "Unknown type";
}
}
// Java 21 Switch Expression with Patterns
public static String patternSwitch(Object obj) {
return switch (obj) {
case String s -> "String: " + s;
case Integer i && i > 0 -> "Positive integer: " + i; // Guarded pattern
case Integer i -> "Non-positive integer: " + i;
case null -> "Null value";
default -> "Unknown type";
};
}
// Exhaustive switching with sealed hierarchies
sealed interface Shape permits Circle, Rectangle, Triangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Triangle(double base, double height) implements Shape {}
public static double calculateArea(Shape shape) {
return switch (shape) {
case Circle(double r) -> Math.PI * r * r;
case Rectangle(double w, double h) -> w * h;
case Triangle(double b, double h) -> 0.5 * b * h;
// No default needed - exhaustive for sealed hierarchy
};
}
}
Advanced Switch Pattern Features
public class AdvancedSwitchPatterns {
// Nested patterns in switch
public static String processNested(Object obj) {
return switch (obj) {
case Person(String name, int age, Address(String street, String city, _)) ->
name + " lives in " + city;
case Employee(String id, Person p, String dept) ->
p.name() + " works in " + dept;
default -> "Unknown";
};
}
// Dominance in pattern cases
public static String dominanceExample(Object obj) {
return switch (obj) {
case String s -> "It's a string: " + s;
case CharSequence cs -> "It's a char sequence: " + cs;
// String pattern must come before CharSequence (more specific first)
case Object o -> "It's an object: " + o;
};
}
// Pattern matching with when guards
public static String guardedPatterns(Object obj) {
return switch (obj) {
case String s when s.length() > 10 -> "Long string: " + s;
case String s when s.isEmpty() -> "Empty string";
case String s -> "Short string: " + s;
case Integer i when i > 100 -> "Large number: " + i;
case Integer i -> "Small number: " + i;
default -> "Other";
};
}
// Dealing with null explicitly
public static String withNullHandling(Object obj) {
return switch (obj) {
case null -> "Null value";
case String s -> "String: " + s;
case Integer i -> "Integer: " + i;
default -> "Other";
};
}
}
4. Unnamed Patterns and Variables (Java 21 Preview)
Unnamed Patterns for Ignoring Components
public class UnnamedPatterns {
record Person(String name, int age, String email, String phone) {}
// Traditional - must name all components even if unused
public static String extractNameTraditional(Object obj) {
if (obj instanceof Person p) {
return p.name();
}
return "Unknown";
}
// Java 21 - ignore unnecessary components with _
public static String extractNameModern(Object obj) {
if (obj instanceof Person(String name, _, _, _)) {
return name;
}
return "Unknown";
}
// Nested unnamed patterns
public static String extractCity(Object obj) {
if (obj instanceof Person(_, _, _, Address(_, String city, _))) {
return city;
}
return "Unknown city";
}
// Multiple levels of ignoring
public static void processComplexObject(Object obj) {
if (obj instanceof Employee(_, Person(String name, _, _), _)) {
System.out.println("Employee: " + name);
}
}
}
Unnamed Variables for Unused Variables
public class UnnamedVariables {
// Traditional - compiler warnings about unused variables
public static void processListTraditional(List<String> list) {
for (String item : list) {
// item is declared but not used
System.out.println("Processing...");
}
}
// Java 21 - use _ for unused variables
public static void processListModern(List<String> list) {
for (String _ : list) { // No compiler warning
System.out.println("Processing...");
}
}
// Multiple unnamed variables
public static void ignoreMultiple() {
try {
int result = riskyOperation();
} catch (Exception _) { // Ignore the exception
System.out.println("Operation failed, but we don't care why");
}
}
// Unnamed variables in try-with-resources
public static void processFile() {
try (var _ = new FileInputStream("data.txt")) {
System.out.println("Processing file...");
} catch (IOException _) {
System.out.println("File error occurred");
}
}
private static int riskyOperation() {
return 42;
}
}
5. Guarded Patterns and Complex Conditions
Advanced Guarded Patterns
public class GuardedPatterns {
record Transaction(String id, double amount, String currency, String status) {}
// Complex conditions with guarded patterns
public static String processTransaction(Object obj) {
return switch (obj) {
case Transaction(String id, double amount, "USD", "PENDING")
when amount > 1000 -> "Large USD pending transaction: " + id;
case Transaction(String id, double amount, "EUR", _)
when amount < 100 -> "Small EUR transaction: " + id;
case Transaction(_, double amount, _, "FAILED")
when amount > 5000 -> "Large failed transaction";
case Transaction t -> "Regular transaction: " + t.id();
case null -> "Null transaction";
};
}
// Combining type patterns with guards
public static String typePatternWithGuards(Object obj) {
return switch (obj) {
case String s when s.startsWith("https://") -> "Secure URL: " + s;
case String s when s.length() == 0 -> "Empty string";
case String s -> "Regular string: " + s;
case Integer i when i % 2 == 0 -> "Even number: " + i;
case Integer i -> "Odd number: " + i;
case List<?> list when list.size() > 10 -> "Large list";
case List<?> list when list.isEmpty() -> "Empty list";
case List<?> list -> "Regular list";
default -> "Other";
};
}
}
6. Practical Real-World Examples
JSON Processing with Pattern Matching
public class JsonProcessor {
// Simulating JSON-like structure with records
sealed interface JsonValue permits JsonObject, JsonArray, JsonString, JsonNumber, JsonBoolean, JsonNull {}
record JsonObject(Map<String, JsonValue> properties) implements JsonValue {}
record JsonArray(List<JsonValue> elements) implements JsonValue {}
record JsonString(String value) implements JsonValue {}
record JsonNumber(double value) implements JsonValue {}
record JsonBoolean(boolean value) implements JsonValue {}
record JsonNull() implements JsonValue {}
public static String jsonToString(JsonValue json) {
return switch (json) {
case JsonObject(var properties) -> {
String content = properties.entrySet().stream()
.map(entry -> "\"" + entry.getKey() + "\": " + jsonToString(entry.getValue()))
.collect(Collectors.joining(", "));
yield "{" + content + "}";
}
case JsonArray(var elements) -> {
String content = elements.stream()
.map(JsonProcessor::jsonToString)
.collect(Collectors.joining(", "));
yield "[" + content + "]";
}
case JsonString(String s) -> "\"" + s + "\"";
case JsonNumber(double n) -> String.valueOf(n);
case JsonBoolean(boolean b) -> String.valueOf(b);
case JsonNull() -> "null";
};
}
public static Optional<Double> extractNumber(JsonValue json, String... path) {
if (path.length == 0) {
return switch (json) {
case JsonNumber(double n) -> Optional.of(n);
default -> Optional.empty();
};
}
return switch (json) {
case JsonObject(Map<String, JsonValue> properties)
when properties.containsKey(path[0]) -> {
String[] remainingPath = Arrays.copyOfRange(path, 1, path.length);
yield extractNumber(properties.get(path[0]), remainingPath);
}
default -> Optional.empty();
};
}
}
Domain-Driven Design with Pattern Matching
public class DomainPatternMatching {
// Domain events
sealed interface DomainEvent
permits UserRegistered, UserUpdated, OrderPlaced, PaymentProcessed {}
record UserRegistered(String userId, String email, String name) implements DomainEvent {}
record UserUpdated(String userId, String name, String phone) implements DomainEvent {}
record OrderPlaced(String orderId, String userId, List<String> items, double amount) implements DomainEvent {}
record PaymentProcessed(String paymentId, String orderId, double amount, boolean success) implements DomainEvent {}
// Event processor using pattern matching
public static void processEvent(DomainEvent event) {
switch (event) {
case UserRegistered(String userId, String email, String name) ->
System.out.println("User registered: " + name + " (" + email + ")");
case UserUpdated(String userId, String name, String phone) ->
System.out.println("User updated: " + name + " - Phone: " + phone);
case OrderPlaced(String orderId, String userId, List<String> items, double amount)
when amount > 1000 ->
System.out.println("Large order placed: " + orderId + " - Amount: $" + amount);
case OrderPlaced(String orderId, String userId, List<String> items, double amount) ->
System.out.println("Order placed: " + orderId);
case PaymentProcessed(String paymentId, String orderId, double amount, boolean success) -> {
if (success) {
System.out.println("Payment successful: " + paymentId);
} else {
System.out.println("Payment failed: " + paymentId);
}
}
}
}
// Event filtering with patterns
public static List<DomainEvent> filterLargeOrders(List<DomainEvent> events) {
return events.stream()
.filter(event -> switch (event) {
case OrderPlaced(_, _, _, double amount) when amount > 500 -> true;
case PaymentProcessed(_, _, double amount, _) when amount > 1000 -> true;
default -> false;
})
.collect(Collectors.toList());
}
}
Configuration Processing
public class ConfigurationProcessor {
record DatabaseConfig(String url, String username, String password, int poolSize) {}
record CacheConfig(String type, int size, Duration ttl) {}
record SecurityConfig(boolean enabled, List<String> allowedOrigins, int timeout) {}
sealed interface Config permits DatabaseConfig, CacheConfig, SecurityConfig {}
public static void validateConfig(Config config) {
switch (config) {
case DatabaseConfig(String url, String username, String password, int poolSize)
when url.startsWith("jdbc:") && poolSize > 0 ->
System.out.println("Valid database config");
case DatabaseConfig _ ->
throw new IllegalArgumentException("Invalid database configuration");
case CacheConfig(String type, int size, Duration ttl)
when List.of("redis", "memcached").contains(type) && size > 0 ->
System.out.println("Valid cache config");
case SecurityConfig(boolean enabled, List<String> origins, int timeout)
when !enabled || (timeout > 0 && !origins.isEmpty()) ->
System.out.println("Valid security config");
default -> throw new IllegalArgumentException("Invalid configuration");
}
}
}
7. Performance Considerations and Best Practices
Performance Patterns
public class PerformancePatterns {
// Efficient pattern ordering
public static String efficientPatternMatching(Object obj) {
return switch (obj) {
// Most frequent cases first
case String s -> "String: " + s;
case Integer i -> "Integer: " + i;
// Less frequent cases later
case Double d -> "Double: " + d;
case List<?> list -> "List size: " + list.size();
// Rare cases last
default -> "Other";
};
}
// Avoid expensive operations in guards
public static String avoidExpensiveGuards(Object obj) {
// ❌ Don't do this - expensive operation in guard
// case String s when expensiveValidation(s) -> ...
// ✅ Do this - move expensive checks after pattern matching
return switch (obj) {
case String s -> {
if (expensiveValidation(s)) {
yield "Valid: " + s;
} else {
yield "Invalid: " + s;
}
}
default -> "Not a string";
};
}
private static boolean expensiveValidation(String s) {
// Simulate expensive operation
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return s.length() > 5;
}
}
Best Practices
public class PatternMatchingBestPractices {
// 1. Use sealed hierarchies for exhaustive matching
sealed interface Result<T> permits Success, Failure {}
record Success<T>(T value) implements Result<T> {}
record Failure<T>(String error) implements Result<T> {}
public static <T> String handleResult(Result<T> result) {
return switch (result) {
case Success<T>(T value) -> "Success: " + value;
case Failure<T>(String error) -> "Error: " + error;
// No default needed - exhaustive
};
}
// 2. Prefer switch expressions over if-else chains
public static String betterReadability(Object obj) {
return switch (obj) {
case String s -> processString(s);
case Integer i -> processInteger(i);
case Double d -> processDouble(d);
default -> "Unknown";
};
}
// 3. Use unnamed patterns to ignore unnecessary components
public static String focusOnRelevantData(Object obj) {
return switch (obj) {
case Person(String name, int age, _, _) -> name + " is " + age + " years old";
default -> "Unknown person";
};
}
// 4. Combine patterns with guards for complex conditions
public static String complexConditions(Object obj) {
return switch (obj) {
case String s when s.length() > 100 -> "Long text";
case String s when s.isEmpty() -> "Empty text";
case String s -> "Short text";
case Integer i when i > 0 -> "Positive";
case Integer i when i < 0 -> "Negative";
case Integer i -> "Zero";
default -> "Other";
};
}
private static String processString(String s) { return "String: " + s; }
private static String processInteger(Integer i) { return "Integer: " + i; }
private static String processDouble(Double d) { return "Double: " + d; }
}
8. Migration Guide from Older Java Versions
Migrating from Java 17 to Java 21
public class MigrationExamples {
// Java 17 - Basic pattern matching
public static String java17Style(Object obj) {
if (obj instanceof String s) {
return "String: " + s;
}
if (obj instanceof Integer i) {
return "Integer: " + i;
}
return "Unknown";
}
// Java 21 - Enhanced pattern matching
public static String java21Style(Object obj) {
return switch (obj) {
case String s -> "String: " + s;
case Integer i -> "Integer: " + i;
case null -> "Null value";
default -> "Unknown";
};
}
// Migrating complex if-else chains
public static String migrateComplexLogic(Object obj) {
// Old style
if (obj instanceof String s) {
if (s.length() > 10) {
return "Long string";
} else {
return "Short string";
}
} else if (obj instanceof Integer i) {
if (i > 0) {
return "Positive";
} else {
return "Non-positive";
}
} else {
return "Other";
}
// New style
return switch (obj) {
case String s when s.length() > 10 -> "Long string";
case String s -> "Short string";
case Integer i when i > 0 -> "Positive";
case Integer i -> "Non-positive";
default -> "Other";
};
}
}
Summary
Java 21's pattern matching evolution brings significant improvements:
instanceofPatterns - Eliminate casting boilerplate- Record Patterns - Deconstruct records elegantly
- Switch Patterns - Powerful type-based dispatch
- Unnamed Patterns - Ignore irrelevant components
- Guarded Patterns - Add conditions to patterns
These features work together to make Java code more expressive, safer, and easier to maintain. The combination of sealed hierarchies, records, and pattern matching provides a robust foundation for writing modern, type-safe Java applications.
The key benefits include:
- Reduced boilerplate - Less casting and manual decomposition
- Improved readability - Intent is clearer
- Enhanced safety - Compiler checks for exhaustiveness
- Better maintainability - Changes to types are reflected at compile time
Pattern matching in Java 21 represents a major step forward in making Java more expressive while maintaining its strong type safety guarantees.