Java 14 → 21: What Changed While You Were Away¶
You last wrote Java around version 8 or 11. The language between then and 21 quietly became a different language. Records replace 40-line POJOs. Sealed classes replace defensive instanceof chains. Pattern matching for switch collapses visitor patterns into eight lines. Virtual threads make blocking I/O free again. This file covers the changes that actually show up in production code and studies — not the academic ones.
Target Java 21 LTS. Java 25 LTS shipped in September 2025 but enterprise adoption still lags by 12–18 months; you will study on 21 codebases for the next two years. Mention 25 if asked, work on 21.
The production-relevance filter¶
Feature |
Java Version |
Production use in 2026 |
study relevance |
|---|---|---|---|
Records |
14 (16 final) |
Everywhere. Replaced most DTOs. |
High |
Sealed classes |
15 preview → 17 |
Domain modeling, ADTs |
Medium-high |
Pattern matching for |
16 |
Everywhere |
High |
Pattern matching for |
21 |
Growing fast |
High |
Text blocks |
15 |
SQL, JSON, HTML literals |
Medium |
Switch expressions |
14 |
Standard |
Medium |
|
10 |
Standard, controversial |
Medium |
Virtual threads |
21 |
Server frameworks adopting |
High (topic) |
Sequenced collections |
21 |
Rarely, but expected |
Low-medium |
Enhanced NullPointerException |
14 |
Automatic, no code change |
Low |
Everything below is worth knowing. Everything not on this list (like Foreign Function API, Vector API, Structured Concurrency preview) is worth knowing exists — nothing more, for now.
1. Records (Java 14 preview, 16 final)¶
A record is an immutable data carrier. The compiler generates the constructor, accessors, equals, hashCode, and toString for you.
Before (the 40-line POJO you wrote 20 times in college):
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int x() { return x; }
public int y() { return y; }
@Override public boolean equals(Object o) { /* ... */ }
@Override public int hashCode() { /* ... */ }
@Override public String toString() { /* ... */ }
}
Now:
public record Point(int x, int y) {}
That’s it. Accessors are p.x() and p.y() (no get prefix). You can add methods, static factories, and a compact constructor for validation:
public record Range(int lo, int hi) {
public Range { // compact constructor
if (lo > hi) throw new IllegalArgumentException();
}
public int span() { return hi - lo; }
public static Range of(int a, int b) { return new Range(Math.min(a,b), Math.max(a,b)); }
}
When to use: DTOs, value objects, tuples, method return types with multiple values, cache keys. When not to use: Entities with mutable state, JPA entities (they need no-arg constructors and setters), anything with lifecycle.
2. Sealed classes (Java 17)¶
A sealed class or interface restricts which classes can extend or implement it. This gives you algebraic data types — the compiler knows the full set of subtypes and can enforce exhaustive switch.
public sealed interface Shape permits Circle, Square, Triangle {}
public record Circle(double r) implements Shape {}
public record Square(double side) implements Shape {}
public record Triangle(double b, double h) implements Shape {}
Combined with pattern matching for switch (below), you get:
double area = switch (shape) {
case Circle c -> Math.PI * c.r() * c.r();
case Square s -> s.side() * s.side();
case Triangle t -> 0.5 * t.b() * t.h();
// no default needed — compiler knows the set is closed
};
Add a new Shape subtype and the compiler flags every non-exhaustive switch. This is the closest Java has come to Kotlin/Scala/Rust-style ADTs.
Modifiers on the permitted types: must be one of final, sealed, or non-sealed. Records are implicitly final. non-sealed opens a branch back up to open extension.
3. Pattern matching for instanceof (Java 16)¶
The cast-after-check ritual is dead.
// Before
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
// After
if (obj instanceof String s) {
System.out.println(s.length());
}
The binding s is only in scope where the compiler can prove it is non-null and correctly typed. You can also negate:
if (!(obj instanceof String s)) return;
s.length(); // s is in scope after the early return
4. Pattern matching for switch (Java 21)¶
The headline Java 21 feature. Combines type patterns, record patterns, and guards.
String describe(Object o) {
return switch (o) {
case null -> "null";
case Integer i when i < 0 -> "negative int: " + i;
case Integer i -> "non-negative int: " + i;
case String s -> "string of length " + s.length();
case int[] arr -> "int array of size " + arr.length;
default -> "something else";
};
}
Record patterns destructure records:
record Point(int x, int y) {}
record Line(Point from, Point to) {}
switch (line) {
case Line(Point(var x1, var y1), Point(var x2, var y2)) ->
System.out.printf("(%d,%d) → (%d,%d)%n", x1, y1, x2, y2);
}
Exhaustiveness is compiler-enforced when the switched type is sealed.
5. Switch expressions (Java 14)¶
Switch that returns a value. Arrow form has no fall-through and no break.
int days = switch (month) {
case JAN, MAR, MAY, JUL, AUG, OCT, DEC -> 31;
case APR, JUN, SEP, NOV -> 30;
case FEB -> 28;
};
Use yield inside a block if you need statements:
int n = switch (x) {
case 1 -> 100;
case 2 -> {
System.out.println("two");
yield 200;
}
default -> 0;
};
6. Text blocks (Java 15)¶
Multi-line string literals. Kills the "\n" + chain.
String json = """
{
"name": "raghul",
"role": "ml-engineer"
}
""";
Indentation is normalized by the compiler based on the closing """. If the closing delimiter is at column 4, four spaces are stripped from each line. Use \ at line end to suppress the newline, and \s for a trailing space that would otherwise be stripped.
Where it matters: SQL queries, JSON test fixtures, HTML fragments, error messages with formatting.
7. var — local-variable type inference (Java 10)¶
var list = new ArrayList<String>(); // inferred: ArrayList<String>
var map = Map.of("a", 1, "b", 2); // inferred: Map<String,Integer>
for (var e : map.entrySet()) { ... } // inferred: Map.Entry<String,Integer>
Rules: local variables only, must have an initializer, not for fields/parameters/return types.
⚠️ What most people get wrong¶
They var everything and destroy readability. var shines when the RHS makes the type obvious (var user = new User(...)) or when the type name is noisy (var it = map.entrySet().iterator()). It hurts when the RHS is a method call whose return type is ambiguous:
var result = process(input); // reader has to jump to process() to know the type
Rule of thumb: if the reader would need to leave this line to know the type, don’t use var.
8. Virtual threads (Java 21)¶
Project Loom’s shipped form. A virtual thread is a lightweight thread scheduled by the JVM onto a small pool of OS carrier threads. You can have millions of them. Blocking I/O on a virtual thread does not block the underlying OS thread.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
}));
} // executor auto-closes and waits for all tasks
Why this matters: the reactive/CompletableFuture complexity you learned exists mostly to avoid blocking OS threads. Virtual threads make plain blocking code performant again. Spring Boot 3.2+, Helidon, Quarkus, and Vert.x all support them.
When NOT to use them: CPU-bound tasks (use ForkJoinPool), tasks that pin the carrier via synchronized blocks or JNI (Java 24 fixes most synchronized pinning — mention it).
Deep dive is in Phase 05. For now: know they exist, know the executor pattern, know they replace 90% of async-callback code you’d otherwise write.
9. Sequenced collections (Java 21)¶
Three new interfaces — SequencedCollection, SequencedSet, SequencedMap — finally give ordered collections a uniform API.
List<Integer> list = new ArrayList<>(List.of(1,2,3));
list.addFirst(0); // [0, 1, 2, 3]
list.getLast(); // 3
list.reversed(); // reversed view, no copy
LinkedHashMap<String,Integer> lhm = new LinkedHashMap<>();
lhm.putFirst("a", 1); // now on the interface
Small, quality-of-life. Not study-central but you look sharp if you mention it when asked “how do I get the first/last of a LinkedHashSet?”.
10. Miscellaneous, worth 30 seconds each¶
Helpful NPE (14):
NullPointerExceptionmessages now say which variable was null.a.b.c.d()NPE tells you it wasc, not just “NPE at line 42”. Free improvement, no code change.Stream.toList()(16): shortcut for.collect(Collectors.toList())returning an unmodifiable list.Files.mismatch(p1, p2)(12): byte-level file comparison, returns first differing byte offset.String.formatted(...)(15):"hello %s".formatted(name)— nicer thanString.format.instanceofpattern inObjects.requireNonNullchains: not a language feature, but you’ll see it more.Deprecated / removed:
Nashorn(JS engine) gone in 15. Security Manager deprecated in 17, terminal in 24. Applets gone.finalize()deprecated for removal.
What you can safely ignore for now¶
Structured Concurrency (still preview through Java 23) — revisit when it goes final
Scoped Values (preview) — replacement for
ThreadLocalin virtual-thread world; not yet finalForeign Function & Memory API (21 final) — only relevant if you interop with native libs
Vector API (still incubator) — SIMD for numeric code; niche
Class-file API (24 final) — for bytecode tooling; niche
Mentioning them in an study: “I know they exist, I’ve read the JEPs, I haven’t used them in production” is the honest and correct answer.
Practice: hand-type these before moving on¶
Type each of these into an empty Scratch.java file, run them, delete, re-type from memory. No copy-paste.
A
record Point(int x, int y)with a compact constructor rejecting negativesA sealed
interface Result<T>withSuccess<T>andFailure<T>recordsA
switchexpression on thatResult<T>that returns a value or a fallbackA method that reads a file into a
List<String>usingFiles.readAllLinesand prints with a text block headerAn
Executors.newVirtualThreadPerTaskExecutor()block that submits 1,000 sleepers and waits
If any of these took more than 5 minutes or required a lookup, re-read the relevant section.
Return to README.md · Next: 02_type_system_and_control_flow.md