Java 17, 21, and Beyond: New Features Guide
Java’s release cadence shifted from 3+ years to every 6 months in 2017, delivering new features faster than ever. Java 17 (September 2021) and Java 21 (September 2023) are the current LTS releases. Together, they introduce the most significant language and runtime changes since Java 8.
Java 17 LTS Features
Sealed Classes (Finalized)
Sealed classes restrict which classes can extend or implement them:
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
// Each permitted subclass must be final, sealed, or non-sealed
public final class Circle implements Shape {
private final double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
---
public record Rectangle(double width, double height) implements Shape {}
public non-sealed class Triangle implements Shape { /* ... */ }Sealed classes enable exhaustive pattern matching — the compiler knows every possible subtype and can verify that switch statements cover all cases. This is particularly valuable for domain modeling where the set of variants is known and fixed, such as AST nodes, payment methods, or network protocol messages.
Pattern Matching for instanceof (Finalized)
// Before Java 17
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
---
// Java 17+
if (obj instanceof String s) {
System.out.println(s.length());
---The pattern variable s is scoped to the enclosing if block. The compiler ensures it is only accessible when the instanceof check succeeds. This eliminates the boilerplate of casting and reduces the risk of ClassCastException.
Records (Finalized)
Records are transparent data carriers with compiler-generated accessors, equals, hashCode, and toString:
public record Point(int x, int y) {}
// Usage
Point p = new Point(10, 20);
int x = p.x(); // accessor — not getX()
int y = p.y();
// Compact constructor for validation
public record Range(int min, int max) {
public Range {
if (min > max) {
throw new IllegalArgumentException("min must be <= max");
}
}
---Records cannot extend other classes, cannot declare instance fields outside the record header, and are implicitly final. They are ideal for DTOs, API responses, event objects, and value objects where equality is based on field values rather than identity.
Text Blocks (Finalized)
String html = """
<html>
<body>
<h1>Hello, %s!</h1>
</body>
</html>
""".formatted(name);
String json = """
{
"name": "Alice",
"age": %d,
"active": true
}
""".formatted(30);Text blocks preserve indentation (stripped by the leading whitespace of the closing delimiter) and support escape sequences. They eliminate the need for string concatenation, newline escaping, and external template files for simple cases.
Switch Expressions (Standard)
// Before: statement switch with fallthrough
String result;
switch (day) {
case MONDAY:
case FRIDAY:
result = "Work day";
break;
case SATURDAY:
case SUNDAY:
result = "Weekend";
break;
default:
result = "Midweek";
---
// Switch expression (Java 14+)
String result = switch (day) {
case MONDAY, FRIDAY -> "Work day";
case SATURDAY, SUNDAY -> "Weekend";
default -> "Midweek";
---;Switch expressions are exhaustive (compiler checks all cases), use arrow syntax to eliminate fallthrough, and are expressions that return a value. They combine naturally with records and sealed classes for complete domain modeling.
Java 21 LTS Features
Virtual Threads (Preview finalized in 21)
Virtual threads are lightweight threads managed by the JVM, not the OS. They enable high concurrency with a simple thread-per-request programming model:
// Before: platform threads (limited to ~10K per GB RAM)
ExecutorService executor = Executors.newFixedThreadPool(200);
executor.submit(() -> handleRequest());
// Java 21+: virtual threads (millions per GB RAM)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> handleRequest());
---
// Create individual virtual threads
Thread vThread = Thread.startVirtualThread(() -> {
// blocking I/O here does not block the OS thread
String data = fetchFromRemote();
process(data);
---);Virtual threads are not faster than platform threads for CPU-bound work, but they dramatically improve throughput for I/O-bound applications. When a virtual thread blocks on I/O, the JVM mounts it off the carrier platform thread and runs another virtual thread. This eliminates thread pool tuning for I/O-heavy workloads.
Pattern Matching for switch (Finalized)
public String describe(Shape shape) {
return switch (shape) {
case Circle c -> "Circle with radius " + c.radius();
case Rectangle r when r.width() == r.height() -> "Square";
case Rectangle r -> "Rectangle " + r.width() + "x" + r.height();
case Triangle t -> "Triangle";
// Exhaustive — no default needed for sealed hierarchies
};
---Patterns can include guards (when clauses). The compiler enforces exhaustiveness and rejects unreachable cases. This is the culmination of multiple JEPs that together make Java’s type-switching as expressive as any modern language.
Record Patterns
// Destructure records in patterns
if (obj instanceof Point(int x, int y)) {
System.out.println("x=" + x + ", y=" + y);
---
// Nested destructuring
if (obj instanceof Rectangle(Point(var x, var y), var size)) {
System.out.println("Top-left: " + x + ", " + y);
---
// In switch
String describe(Shape shape) {
return switch (shape) {
case Circle(var radius) -> "Circle radius=" + radius;
case Rectangle(var w, var h) -> w + "x" + h;
};
---Record patterns deconstruct record instances into their components. They compose with pattern matching for switch, enabling deeply nested structural matching without nested if-instanceof chains.
String Templates (Preview)
String name = "Alice";
int age = 30;
// Template processor
String message = STR."Hello, \{name}! You are \{age} years old.";String templates combine literal text with embedded expressions. The STR template processor handles interpolation at compile time, producing efficient code. Custom template processors can validate or transform template content (e.g., SQL injection prevention).
Sequenced Collections
// New interfaces for ordered collections
SequencedCollection<String> list = new ArrayList<>();
list.addFirst("a");
list.addLast("z");
String first = list.getFirst(); // "a"
String last = list.getLast(); // "z"
SequencedSet<String> set = new LinkedHashSet<>();
SequencedMap<String, Integer> map = new LinkedHashMap<>();The SequencedCollection, SequencedSet, and SequencedMap interfaces standardize operations for collections with a defined encounter order — addFirst, addLast, getFirst, getLast, reversed. Previously, these were available only on specific implementations.
Features in Progress
Structured Concurrency (Preview in 21)
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser());
Future<Integer> orders = scope.fork(() -> fetchOrderCount());
scope.join(); // waits for all forks
scope.throwIfFailed(); // propagates exceptions
return new UserData(user.resultNow(), orders.resultNow());
---Structured concurrency treats tasks in the same scope as a single unit of work. If one subtask fails, the scope cancels the others. This eliminates thread leaks from forgotten subtasks and makes error propagation explicit.
Scoped Values (Incubating)
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
void handle(Request req) {
ScopedValue.where(REQUEST_ID, req.id())
.run(() -> {
// REQUEST_ID is available here and in all callees
process();
});
---
void process() {
String id = REQUEST_ID.get(); // immutable, no mutation possible
---Scoped values are an alternative to ThreadLocal that guarantees immutability and inherits values across virtual thread boundaries. They prevent memory leaks common with ThreadLocal in thread pools.
Migration Considerations
From Java 11 to 17/21
- Remove
--illegal-access=permitflags — encapsulation is now the default - Update build tools (Maven 3.8+, Gradle 7.3+)
- Migrate from
finalize()toCleanerorCleaner+ phantom references - Replace deprecated security APIs with modern alternatives
- Test reflection-heavy libraries (many need module-info or
--add-opens)
From Java 8 to 17/21
This is a larger migration that touches the module system:
- First migrate to Java 11 (module path optional)
- Enable
--illegal-access=permitfor discovery - Fix encapsulation violations incrementally
- Migrate to Java 17 (module path required for strong encapsulation)
- Update application server / framework / build plugins
- Replace removed APIs (Nashorn, CORBA, some XML modules)
FAQ
Q: Should I upgrade from Java 17 to Java 21? A: Yes, for most applications. Java 21 is the latest LTS with significant improvements in concurrency (virtual threads) and pattern matching.
Q: Are virtual threads a replacement for reactive programming? A: Yes, for most use cases. Virtual threads let you use simple blocking I/O code and still handle high concurrency. Reactive frameworks remain useful for extreme throughput requirements and backpressure management.
Q: What about Java 22+ non-LTS releases? A: Non-LTS releases (18, 19, 20, 22) are fine for development and short-lived services. Production deployments should prefer LTS releases (17, 21).
Q: Do records replace Lombok? A: Partially. Records cover @Data/@Value use cases but cannot extend other classes, cannot have instance fields outside the header, and cannot be JPA entities.
Q: Can I mix Java 8 and Java 21 code in the same project? A: No. A single source set uses one language version. Multi-module projects can use different versions per module.
Q: Are string templates production-ready? A: String templates are a preview feature. They may change in future releases. Use them in development but avoid in production libraries.
Q: Do I need to learn Kotlin if Java 21 has all these features? A: Not necessarily. Java 21 closes the gap significantly, but Kotlin still offers null safety by default, extension functions, and more concise coroutines.
For a comprehensive overview, read our article on Java Basics Tutorial.
For a comprehensive overview, read our article on Java Build Tools.