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.