Java 21 Pattern Matching Evolution: Comprehensive Guide

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

  1. Pattern Matching for instanceof
  2. Record Patterns
  3. Pattern Matching for switch
  4. Unnamed Patterns and Variables
  5. Guarded Patterns
  6. 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:

  1. instanceof Patterns - Eliminate casting boilerplate
  2. Record Patterns - Deconstruct records elegantly
  3. Switch Patterns - Powerful type-based dispatch
  4. Unnamed Patterns - Ignore irrelevant components
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper