JDK 20’s Position
JDK 20, released in March 2023, is a non-LTS release. While it may seem light on flashy new features on its own, it actually serves as the final rehearsal for JDK 21 LTS. Features like Record Patterns, Virtual Threads, and Structured Concurrency that first appeared as previews in JDK 19 went through their second previews with refined APIs, incorporating community feedback in the process.
This article covers the key JEPs (JDK Enhancement Proposals) included in JDK 20 with runnable examples.
Record Patterns (Second Preview, JEP 432)
Record Patterns is a feature that destructures record types in instanceof and switch. It first appeared as a preview in JDK 19, and JDK 20 strengthened nested record destructuring and generic record support.
public class RecordPatternExample {
// Record representing coordinates
record Point(int x, int y) {}
// Nested record representing a line segment
record Line(Point start, Point end) {}
// Generic record
record Pair<T>(T first, T second) {}
static String describeLine(Object obj) {
// Nested record destructuring: extract Points inside Line all at once
if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
return "Line length: %.2f".formatted(length);
}
return "Not a Line";
}
public static void main(String[] args) {
Line line = new Line(new Point(0, 0), new Point(3, 4));
System.out.println(describeLine(line));
// Output: Line length: 5.00
// Generic record pattern matching
Pair<String> pair = new Pair<>("Hello", "World");
if (pair instanceof Pair<String>(var first, var second)) {
System.out.println(first + " " + second);
}
// Output: Hello World
}
}
Previously, type checking with instanceof, casting, then calling getters required three steps. With Record Patterns, type checking and field extraction happen simultaneously in one line. Being able to destructure nested records at once makes DTO and domain object processing much cleaner.
Pattern Matching for switch (Fourth Preview, JEP 433)
The fourth preview of using type patterns and when guards in switch statements. In JDK 20, the syntax was further stabilized in preparation for the JDK 21 final release.
public class SwitchPatternExample {
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 {}
// switch pattern matching + Record Patterns + when guard combination
static String describeShape(Shape shape) {
return switch (shape) {
case Circle(var r) when r > 100 -> "Large circle (radius: %.1f)".formatted(r);
case Circle(var r) -> "Circle (radius: %.1f)".formatted(r);
case Rectangle(var w, var h) when w == h -> "Square (side: %.1f)".formatted(w);
case Rectangle(var w, var h) -> "Rectangle (%.1f x %.1f)".formatted(w, h);
case Triangle(var b, var h) -> "Triangle (base: %.1f, height: %.1f)".formatted(b, h);
};
}
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5.0),
new Circle(150.0),
new Rectangle(10.0, 10.0),
new Rectangle(3.0, 7.0),
new Triangle(6.0, 4.0)
};
for (Shape s : shapes) {
System.out.println(describeShape(s));
}
// Output:
// Circle (radius: 5.0)
// Large circle (radius: 150.0)
// Square (side: 10.0)
// Rectangle (3.0 x 7.0)
// Triangle (base: 6.0, height: 4.0)
}
}
When using sealed interface with pattern matching, the compiler checks all subtypes, so no default branch is needed. Adding a new Shape implementation will immediately produce a compile error.
Scoped Values (Incubator, JEP 429)
ScopedValue is an alternative to ThreadLocal, a mechanism for sharing values only within a specific scope. It was designed to solve ThreadLocal’s memory issues in Virtual Thread environments.
Key differences from ThreadLocal:
| Item | ThreadLocal | ScopedValue |
|---|---|---|
| Lifetime | Entire thread | Within specified scope |
| Mutation | Mutable at any time | Immutable after binding |
| Inheritance | Requires InheritableThreadLocal | Auto-inherited (integrated with Structured Concurrency) |
| Memory | Accumulates per thread | Auto-released when scope ends |
import java.util.concurrent.Executors;
public class ScopedValueExample {
// ScopedValue declaration (JDK 20 incubator)
// Requires --enable-preview --add-modules jdk.incubator.concurrent at runtime
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
static void processRequest() {
// Read value from ScopedValue
String user = CURRENT_USER.get();
System.out.println("Processing request - user: " + user);
handleDatabase();
}
static void handleDatabase() {
// Accessible anywhere in the call stack (no parameter passing needed)
String user = CURRENT_USER.get();
System.out.println("Executing DB query - user: " + user);
}
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Bind ScopedValue for each request
executor.submit(() -> {
ScopedValue.where(CURRENT_USER, "alice")
.run(() -> processRequest());
return null;
});
executor.submit(() -> {
ScopedValue.where(CURRENT_USER, "bob")
.run(() -> processRequest());
return null;
});
}
// Output:
// Processing request - user: alice
// Executing DB query - user: alice
// Processing request - user: bob
// Executing DB query - user: bob
}
}
With the ScopedValue.where(KEY, value).run(...) pattern, all code within the run() block can access the value via KEY.get(). When the block ends, the binding is automatically released, preventing memory leaks.
Virtual Threads (Second Preview) and Structured Concurrency
Virtual Threads (JEP 436) had their API nearly finalized in JDK 19’s first preview, with only minor improvements in JDK 20. For detailed usage and caveats, see the Virtual Threads Complete Guide post.
Structured Concurrency (JEP 437) is an API that groups multiple concurrent tasks into a single unit. When a parent task is cancelled, child tasks are automatically cancelled too, and child exceptions propagate to the parent. It’s the concept of managing concurrency like structural code blocks (try-finally).
Foreign Function & Memory API (Second Preview, JEP 434)
The second preview of the FFM API designed to replace JNI (Java Native Interface). It enables safe and efficient native library calls and off-heap memory management.
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class FFMExample {
public static void main(String[] args) throws Throwable {
// Off-heap memory allocation and management
try (Arena arena = Arena.ofConfined()) {
// Allocate C string in off-heap memory
MemorySegment cString = arena.allocateFrom("Hello from Java FFM!");
// Call native strlen function
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
long length = (long) strlen.invoke(cString);
System.out.println("String length: " + length);
// Output: String length: 20
}
// Off-heap memory auto-released when Arena ends
}
}
Since Arena manages memory lifecycle, wrapping it with try-with-resources prevents native memory leaks. Safety is greatly improved compared to manually calling free() in JNI.
Summary
JDK 20 may feel like a “release full of previews” when viewed independently, but it was the final stabilization process for JDK 21 LTS.
| JEP | Feature | Status | JDK 21 Progress |
|---|---|---|---|
| 432 | Record Patterns | 2nd Preview | -> Final (JEP 440) |
| 433 | Pattern Matching for switch | 4th Preview | -> Final (JEP 441) |
| 429 | Scoped Values | Incubator | -> Preview (JEP 446) |
| 436 | Virtual Threads | 2nd Preview | -> Final (JEP 444) |
| 437 | Structured Concurrency | 2nd Incubator | -> Preview (JEP 453) |
| 434 | Foreign Function & Memory API | 2nd Preview | -> 3rd Preview (JEP 442) |
Developers who experimented with preview features in JDK 20 will find JDK 21 migration much smoother. In particular, Record Patterns and Pattern Matching for switch had almost no syntax changes between JDK 20 and 21, so code written for JDK 20 can be used as-is. Even for non-LTS releases, actively experimenting in development/test environments that aren’t intended for production is recommended.