Skip to main content
Java

Java 23: Pattern Matching Evolution and More Previews

Ravinder··8 min read
JavaPattern MatchingPrimitive TypesJava 23
Share:
Java 23: Pattern Matching Evolution and More Previews

Java 23: Pattern Matching and Structural Patterns

Java 23 (September 2024) advances pattern matching with primitive type patterns and array patterns. It's a non-LTS release continuing Java's modern language evolution.

1. Pattern Matching for Primitive Types (Preview)

Match on primitive values directly.

Before Java 23:

// Had to work with boxed types or traditional approaches
int age = 25;
 
// Limited pattern matching on primitives
if (age >= 18) {
    System.out.println("Adult");
} else {
    System.out.println("Minor");
}
 
// Had to use nested switches or if-else chains
Object value = 10;
if (value instanceof Integer i) {
    if (i >= 0) {
        System.out.println("Positive");
    }
}

With Primitive Patterns (Java 23+):

// Direct pattern matching on primitives
int age = 25;
 
String category = switch (age) {
    case int i when i < 13 -> "Child";
    case int i when i < 18 -> "Teen";
    case int i when i < 65 -> "Adult";
    case int i -> "Senior";
};
 
// Type patterns for primitives
double score = 85.5;
String grade = switch (score) {
    case double d when d >= 90 -> "A";
    case double d when d >= 80 -> "B";
    case double d when d >= 70 -> "C";
    case double _ -> "Below C";
};
 
// Complex primitive matching
long timestamp = System.currentTimeMillis();
String timeCategory = switch (timestamp) {
    case long t when t > 0 && t < 1000000000000L -> "Past";
    case long t when t >= 1000000000000L && t < 2000000000000L -> "Present";
    case long _ -> "Future";
};
 
// Multiple primitive types
Object measurement = 42;
String result = switch (measurement) {
    case int i when i > 100 -> "High integer";
    case double d when d > 100.0 -> "High decimal";
    case long l when l > 100L -> "High long";
    case _ -> "Other";
};

2. Array Patterns (Preview)

Match array structures directly.

// Array pattern matching
int[] numbers = {1, 2, 3};
 
// Basic array pattern
if (numbers instanceof int[] arr && arr.length == 3) {
    System.out.println("Array of 3 elements");
}
 
// Decomposing arrays with patterns
int[] triple = {10, 20, 30};
if (triple instanceof int[] {0: int first, 1: int second, 2: int third}) {
    System.out.println(first + second + third);
}
 
// Array patterns in switch (Preview)
var result = switch (numbers) {
    case int[] {length: 0} -> "Empty";
    case int[] {0: int first, length: 1} -> "Single: " + first;
    case int[] {0: int a, 1: int b} -> "At least two: " + a + ", " + b;
    case int[] _ -> "More elements";
};
 
// Nested array patterns
String[][] matrix = {
    {"a", "b"},
    {"c", "d"},
    {"e", "f"}
};
 
if (matrix instanceof String[][] { 0: String[] {0: String val} }) {
    System.out.println("First element: " + val); // "a"
}
 
// Working with Object arrays
Object[] mixed = {"hello", 42, 3.14};
var typeCheck = switch (mixed) {
    case Object[] {0: String s, 1: Integer i, 2: Double d} 
        -> String.format("%s, %d, %.2f", s, i, d);
    case _ -> "Unexpected format";
};

3. Record Patterns Refinement

Improved deconstruction of records.

// Record definitions
sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
 
// Advanced record pattern matching (Java 23)
public class ShapeProcessor {
    
    public double getArea(Shape shape) {
        return switch (shape) {
            case Circle(double r) -> Math.PI * r * r;
            case Rectangle(double w, double h) -> w * h;
        };
    }
    
    // Nested record patterns
    record Point(double x, double y) {}
    record Line(Point start, Point end) {}
    
    public double getDistance(Line line) {
        return switch (line) {
            case Line(Point(double x1, double y1), 
                     Point(double x2, double y2))
                -> Math.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1));
        };
    }
    
    // Guards with record patterns
    public String categorizeShape(Shape shape) {
        return switch (shape) {
            case Circle(double r) when r < 5 -> "Small circle";
            case Circle(double r) when r >= 5 && r < 20 -> "Medium circle";
            case Circle(double r) -> "Large circle";
            case Rectangle(double w, double h) when w == h -> "Square";
            case Rectangle(double w, double h) -> "Rectangle";
        };
    }
}

4. Scoped Values (Incubating)

Safer alternative to ThreadLocal for sharing data.

// Define scoped values
static final ScopedValue<String> USER = ScopedValue.newInstance();
static final ScopedValue<Integer> TRANSACTION_ID = ScopedValue.newInstance();
 
public class ScopedValueExample {
    
    public void processRequest(String userName) {
        // Bind scoped values for this execution context
        ScopedValue.where(USER, userName)
            .where(TRANSACTION_ID, generateTransactionId())
            .run(this::handleRequest);
    }
    
    // Access scoped values (thread-safe, context-aware)
    void handleRequest() {
        String currentUser = USER.get(); // No null checks needed
        int txId = TRANSACTION_ID.get();
        
        System.out.println("Processing for: " + currentUser + 
                         ", TxID: " + txId);
        
        // Call other methods - values are accessible
        logActivity();
        updateDatabase();
    }
    
    void logActivity() {
        System.out.println("User: " + USER.get()); // Still accessible
    }
    
    void updateDatabase() {
        // Scoped value provides context without parameter passing
        String user = USER.get();
        // ... database work
    }
    
    // Comparison with ThreadLocal
    public void threadLocalApproach() {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        try {
            threadLocal.set("user");
            // Must pass around or use ThreadLocal
            String value = threadLocal.get();
        } finally {
            threadLocal.remove(); // Must clean up
        }
    }
}

Advantages over ThreadLocal:

public class ScopedVsThreadLocal {
    
    // ThreadLocal issues
    static ThreadLocal<String> userTL = new ThreadLocal<>();
    
    public void problematicThreadLocal() {
        userTL.set("alice");
        
        // 1. Memory leak risk if not removed
        // 2. Visible to child threads in thread pool
        // 3. Requires try-finally for cleanup
        
        try {
            doWork();
        } finally {
            userTL.remove(); // Easy to forget
        }
    }
    
    // Scoped Values advantages
    static final ScopedValue<String> USER = ScopedValue.newInstance();
    
    public void betterScopedValue() {
        ScopedValue.where(USER, "alice")
            .run(this::doWork);
        
        // Automatically cleaned up after run()
        // Not visible to child threads
        // Type-safe, null-safe by binding
    }
    
    void doWork() {
        String user = USER.get(); // Never null in scope
    }
}

5. Stream API Enhancements

New stream operations and refinements.

// New stream methods
public class StreamEnhancements {
    
    public void demonstrateNewMethods() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        
        // Stream.ofNullable - handle nullable items
        String nullableValue = null;
        long count = Stream.ofNullable(nullableValue)
            .count(); // 0 instead of NPE
        
        // Stream.mapMulti - mapping to variable number of results
        Stream<String> expanded = numbers.stream()
            .mapMulti((num, consumer) -> {
                consumer.accept("num: " + num);
                consumer.accept("squared: " + (num * num));
            });
        // Result: "num: 1", "squared: 1", "num: 2", "squared: 4", ...
        
        // Collectors improvements
        var grouped = numbers.stream()
            .collect(Collectors.groupingBy(
                n -> n % 2 == 0 ? "even" : "odd",
                Collectors.averagingDouble(Integer::doubleValue)
            ));
        // Result: {even=4.0, odd=3.0}
        
        // Collectors.teeing - two collectors in one operation
        record Stats(double average, long count) {}
        var stats = numbers.stream()
            .collect(Collectors.teeing(
                Collectors.averagingDouble(Integer::doubleValue),
                Collectors.counting(),
                Stats::new
            ));
    }
}

6. Garbage Collection Improvements

Enhanced GC performance and control.

// GC improvements in Java 23
public class GCDemo {
    
    public void demonstrateGCEnhancements() {
        // G1GC improvements
        System.setProperty("XX:+G1UseAdaptiveIHOP", "true");
        
        // Generational ZGC (if available)
        System.setProperty("XX:+UseZGC", "true");
        System.setProperty("XX:ZGenerational", "true");
        
        // Monitoring
        com.sun.management.OperatingSystemMXBean osBean =
            (com.sun.management.OperatingSystemMXBean) 
            ManagementFactory.getOperatingSystemMXBean();
        
        System.out.println("GC count: " + 
            ManagementFactory.getGarbageCollectorMXBeans()
                .stream()
                .mapToLong(b -> b.getCollectionCount())
                .sum()
        );
    }
}

7. Foreign Function & Memory API (Refinement)

Continued refinement of native interoperability.

// FFM API improvements (Preview)
public class FFMDemo {
    
    public void nativeCall() {
        // Safer native calls
        try (Arena arena = Arena.ofConfined()) {
            // Allocate native memory
            MemorySegment segment = arena.allocate(
                ValueLayout.JAVA_INT,
                0x0123_4567
            );
            
            // Use native memory safely
            int value = segment.getAtIndex(ValueLayout.JAVA_INT, 0);
            
            // Automatic cleanup when arena closes
        }
    }
}

Developer Impact

Advanced Pattern Matching:

  • Complex data structure handling
  • Exhaustive pattern checking
  • More expressive domain modeling

Scoped Values:

  • Context passing without ThreadLocal issues
  • Better for virtual threads
  • Cleaner async code

Stream API:

  • More flexible data processing
  • Better functional composition

Pros and Cons

Pros ✅

  • Pattern Matching: Powerful data destructuring
  • Primitive Patterns: Direct value matching
  • Scoped Values: ThreadLocal replacement
  • Safety: Exhaustive checking in switch
  • Expression: Concise code for complex logic
  • Virtual Thread Friendly: Better async support

Cons ❌

  • Learning Curve: Complex pattern syntax
  • Preview Features: May change
  • No LTS: 6-month support
  • Virtual Machine Load: Complex patterns compile to bytecode
  • Debugging: Pattern matching can be hard to trace
  • IDE Support: Needs up-to-date tooling

Real-World Example

// API response processing with pattern matching
sealed interface ApiResponse permits SuccessResponse, ErrorResponse {}
record SuccessResponse(Object data) implements ApiResponse {}
record ErrorResponse(int code, String message) implements ApiResponse {}
 
public class ApiHandler {
    
    public void handleResponse(ApiResponse response) {
        String result = switch (response) {
            case SuccessResponse(Object data) when data instanceof String s 
                -> "Success: " + s;
            case SuccessResponse(Object data) when data instanceof Map m 
                -> "Success: " + m.size() + " items";
            case ErrorResponse(int code, String msg) when code >= 500 
                -> "Server error: " + msg;
            case ErrorResponse(int code, String msg) 
                -> "Client error: " + msg;
        };
        
        System.out.println(result);
    }
}

Conclusion

Java 23 continues maturation of pattern matching. Primitive patterns and array patterns enable expressive data handling. Scoped values provide cleaner context management. While non-LTS, Java 23 shows strong progress toward comprehensive pattern matching and better async support.

Recommendation:

  • Adopt pattern matching patterns in Java 21+ codebases
  • Experiment with Scoped Values for virtual thread projects
  • Wait for Java 25/26 for production-ready advanced patterns
  • Watch for feature finalization in upcoming LTS releases