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