Lesson 67 of 71
What Changed from Java 17 to Java 25
This lesson is a map of the changes from Java 17 to Java 25. Every feature in it is taught in an earlier lesson of this course; each row links there. Three examples combine several features in one program, and a checklist lists what breaks or changes when a code base moves from 17 to 21 to 25.
| Part | Content |
|---|---|
| Release cadence | LTS releases, preview and incubator status |
| Release map | Java 17 to 25, one line per JEP |
| Examples | features combined in one program, javac --release checks, runtime checks |
| Migration | removed flags, Security Manager, encapsulation, charset, finalization, biased locking, ZGC |
Release cadence and status words
A feature release ships every six months, in March and September. A long-term support (LTS) release is one that vendors patch for several years; since Java 17 an LTS ships every two years: 17 (September 2021), 21 (September 2023), 25 (September 2025).
A JEP (JDK Enhancement Proposal) is the document that defines one change to the JDK. Each JEP targets a release and has a status in it:
| Status | Meaning | How to use it |
|---|---|---|
| Final | standard, follows compatibility rules | nothing |
| Preview | complete but may change or be removed | --enable-preview at compile and run time, class files tied to that release |
| Incubator | API in a jdk.incubator.* module |
--add-modules jdk.incubator.x |
| Experimental | VM feature off by default | -XX:+UnlockExperimentalVMOptions |
A feature usually goes through one or more previews before it becomes final, and the JEP number changes with each round. The tables below give the JEP number for the release in that row.
Release map
JEP numbers and statuses follow the OpenJDK release pages. Example 2 confirms the release of six final language and API features with javac --release N on JDK 25, and the preview status of structured concurrency, primitive patterns, and stable values in Java 25 was confirmed from the compiler's preview errors. The other numbers were not machine-checked here.
Java 17 (LTS, September 2021)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 409 | Sealed classes | final | Records and Sealed Types |
| 403 | Strongly encapsulate JDK internals (--illegal-access removed) |
final | Packages, Modules, and Reflection |
| 306 | Always-strict floating point (strictfp redundant) |
final | Types, Variables, and Expressions |
| 356 | Enhanced pseudo-random generators (RandomGenerator) |
final | not covered |
| 415 | Context-specific deserialization filters | final | Streams, Readers, and Files |
| 411 | Deprecate the Security Manager for removal | final | this lesson, migration checklist |
| 406 | Pattern matching for switch |
preview | Pattern Matching |
| 412 | Foreign Function and Memory API | incubator | The Foreign Function and Memory API |
HexFormat is also new in Java 17 (no JEP); Strings and Text covers it. Records and instanceof patterns were finalized in Java 16 (JEPs 395 and 394).
Java 18 (March 2022)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 400 | UTF-8 by default | final | Strings and Text |
| 408 | Simple web server (jwebserver) |
final | not covered |
| 413 | Code snippets in Javadoc (@snippet) |
final | not covered |
| 416 | Core reflection reimplemented with method handles | final | Packages, Modules, and Reflection |
| 421 | Deprecate finalization for removal | final | Immutability, Defensive Copying, and Object Lifecycle |
| 420 | Pattern matching for switch |
second preview | Pattern Matching |
The biased locking code was removed from HotSpot in Java 18 (a bug-tracker change, not a JEP); Threads, synchronized, wait and notify explains it.
Java 19 (September 2022)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 425 | Virtual threads | preview | Virtual Threads in Depth |
| 405 | Record patterns | preview | Pattern Matching |
| 424 | Foreign Function and Memory API | preview | The Foreign Function and Memory API |
| 428 | Structured concurrency | incubator | Virtual Threads in Depth |
Java 20 (March 2023)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 436 | Virtual threads | second preview | Virtual Threads in Depth |
| 432, 433 | Record patterns, pattern matching for switch |
second and fourth preview | Pattern Matching |
| 429 | Scoped values | incubator | Virtual Threads in Depth |
Java 20 has no new final language or API JEPs.
Java 21 (LTS, September 2023)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 444 | Virtual threads | final | Virtual Threads in Depth |
| 441 | Pattern matching for switch |
final | Pattern Matching |
| 440 | Record patterns | final | Pattern Matching |
| 431 | Sequenced collections (getFirst, reversed) |
final | Lists, Deques, and Iteration |
| 439 | Generational ZGC (-XX:+ZGenerational) |
final | ZGC and Shenandoah |
| 451 | Prepare to disallow dynamic loading of agents (warning) | final | not covered |
| 443 | Unnamed patterns and variables | preview | Types, Variables, and Expressions |
| 445 | Unnamed classes and instance main |
preview | Types, Variables, and Expressions |
| 446, 453 | Scoped values, structured concurrency | preview | Virtual Threads in Depth |
| 442 | Foreign Function and Memory API | third preview | The Foreign Function and Memory API |
| 430 | String templates | preview, later withdrawn | not covered |
Math.clamp, StringBuilder.repeat, and String.splitWithDelimiters are also Java 21 additions without their own JEP; Example 1 uses Math.clamp, and Strings and Text covers the string methods.
Java 22 (March 2024)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 456 | Unnamed variables and patterns (_) |
final | Types, Variables, and Expressions |
| 454 | Foreign Function and Memory API | final | The Foreign Function and Memory API |
| 423 | Region pinning for G1 | final | G1 in Depth |
| 458 | Launch multi-file source-code programs | final | not covered |
| 461 | Stream gatherers | preview | Gatherers, Parallel Streams, and Stream Cost |
| 447 | Statements before super(...) |
preview | Classes, Interfaces, and Inheritance |
| 457 | Class-File API | preview | not covered |
Java 23 (September 2024)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 474 | ZGC generational mode by default (ZGenerational deprecated) |
final | ZGC and Shenandoah |
| 471 | Deprecate memory-access methods in sun.misc.Unsafe for removal |
final | The Foreign Function and Memory API |
| 467 | Markdown documentation comments (///) |
final | not covered |
| 455 | Primitive types in patterns, instanceof, and switch |
preview | Pattern Matching |
| 476 | Module import declarations | preview | Packages, Modules, and Reflection |
| 473 | Stream gatherers | second preview | Gatherers, Parallel Streams, and Stream Cost |
String templates were dropped in Java 23 and are not in later releases.
Java 24 (March 2025)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 485 | Stream gatherers | final | Gatherers, Parallel Streams, and Stream Cost |
| 491 | Synchronize virtual threads without pinning | final | Virtual Threads in Depth |
| 484 | Class-File API | final | not covered |
| 486 | Permanently disable the Security Manager | final | this lesson, migration checklist |
| 490 | ZGC: remove the non-generational mode | final | ZGC and Shenandoah |
| 472 | Prepare to restrict the use of JNI (warning) | final | The Foreign Function and Memory API |
| 483 | Ahead-of-time class loading and linking | final | Safepoints, Warmup, and Deoptimization |
| 450 | Compact object headers | experimental | Object Layout and Allocation |
| 404 | Generational Shenandoah | experimental | ZGC and Shenandoah |
Java 25 (LTS, September 2025)
| JEP | Feature | Status | Lesson |
|---|---|---|---|
| 512 | Compact source files and instance main (java.lang.IO) |
final | Types, Variables, and Expressions |
| 511 | Module import declarations | final | Packages, Modules, and Reflection |
| 513 | Flexible constructor bodies (statements before super(...)) |
final | Classes, Interfaces, and Inheritance |
| 506 | Scoped values | final | Virtual Threads in Depth |
| 519 | Compact object headers (-XX:+UseCompactObjectHeaders, off by default) |
product | Object Layout and Allocation |
| 521 | Generational Shenandoah | product | ZGC and Shenandoah |
| 514, 515 | AOT command-line ergonomics, AOT method profiling | final | Safepoints, Warmup, and Deoptimization |
| 505 | Structured concurrency | fifth preview | Virtual Threads in Depth |
| 507 | Primitive types in patterns | third preview | Pattern Matching |
| 502 | Stable values | preview | not covered |
| 508 | Vector API | tenth incubator | Escape Analysis, Loop Optimizations, and Generated Assembly |
The "not covered" rows are outside the scope of this course. Lessons not linked above that use only Java 17 APIs still apply unchanged on Java 25.
Example 1: a Java 25 compact source file with features from 17 to 25
A compact source file has methods and fields without a class declaration, and it imports every package that java.base exports. This one uses a sealed interface and records (17), pattern switch with record patterns and sequenced collections (21), _ (22), a gatherer (24), and IO.println (25).
sealed interface Order permits Limit, Market {}
record Limit(String side, long price, int qty) implements Order {}
record Market(String side, int qty) implements Order {}
String describe(Order o) {
return switch (o) {
case Limit(var side, var price, var qty) -> side + " " + qty + " @ " + price;
case Market(var side, _) -> side + " at market";
};
}
void main() {
List<Order> orders = new ArrayList<>(List.of(
new Limit("BUY", 101, 5), new Market("SELL", 3), new Limit("SELL", 103, 7)));
orders.addFirst(new Market("BUY", 1));
IO.println("first: " + describe(orders.getFirst()));
IO.println("last: " + describe(orders.getLast()));
orders.reversed().forEach(o -> IO.println(" " + describe(o)));
List<List<Integer>> pairs = orders.stream()
.map(o -> switch (o) { case Limit l -> l.qty(); case Market m -> m.qty(); })
.gather(Gatherers.windowFixed(2))
.toList();
IO.println("qty pairs: " + pairs);
IO.println("clamped: " + Math.clamp(250L, 0, 100));
}first: BUY at market
last: SELL 7 @ 103
SELL 7 @ 103
SELL at market
BUY 5 @ 101
BUY at market
qty pairs: [[1, 5], [3, 7]]
clamped: 100The switch has no default because the interface is sealed and both permitted records are handled; adding a third record makes it a compile error. reversed() returns a view, so no copy is made. Gatherers.windowFixed(2) groups elements in lists of two. Math.clamp(long, int, int) returns an int within the bounds.
Example 2: javac --release shows where each feature starts
javac --release N compiles against the language and API of release N, which is the reliable way to check which release made a feature final. The program below compiles small sources in memory with the javax.tools compiler API and prints whether each one compiles.
import javax.tools.*;
import java.net.URI;
import java.util.*;
public class ReleaseCheck {
static boolean compiles(String code, int release) {
JavaCompiler javac = ToolProvider.getSystemJavaCompiler();
String name = code.startsWith("void") ? "Main" : "T";
JavaFileObject src = new SimpleJavaFileObject(
URI.create("string:///" + name + ".java"), JavaFileObject.Kind.SOURCE) {
@Override public CharSequence getCharContent(boolean ignore) { return code; }
};
DiagnosticCollector<JavaFileObject> diags = new DiagnosticCollector<>();
var fm = javac.getStandardFileManager(diags, null, null);
List<String> opts = List.of("--release", String.valueOf(release), "-d",
System.getProperty("java.io.tmpdir") + "/release-check");
return javac.getTask(null, fm, diags, opts, null, List.of(src)).call();
}
public static void main(String[] args) {
Map<String, String> features = new LinkedHashMap<>();
features.put("sealed (409)", "sealed interface T permits A {} final class A implements T {}");
features.put("record pattern switch (440/441)",
"class T { record P(int x) {} int f(Object o) { return switch (o) { case P(int x) -> x; default -> 0; }; } }");
features.put("unnamed variable _ (456)",
"class T { int f(int[] a) { int n = 0; for (int _ : a) n++; return n; } }");
features.put("gatherers (485)",
"class T { Object f() { return java.util.stream.Stream.of(1).gather(java.util.stream.Gatherers.windowFixed(2)).toList(); } }");
features.put("flexible constructor (513)",
"class T { int a; T(int v) { if (v < 0) throw new IllegalArgumentException(); super(); a = v; } }");
features.put("compact source file (512)", "void main() { IO.println(1); }");
int[] releases = {17, 21, 22, 24, 25};
for (var e : features.entrySet()) {
StringBuilder line = new StringBuilder(String.format("%-33s", e.getKey()));
for (int r : releases) {
line.append(r).append(compiles(e.getValue(), r) ? ":yes " : ":no ");
}
System.out.println(line.toString().strip());
}
}
}sealed (409) 17:yes 21:yes 22:yes 24:yes 25:yes
record pattern switch (440/441) 17:no 21:yes 22:yes 24:yes 25:yes
unnamed variable _ (456) 17:no 21:no 22:yes 24:yes 25:yes
gatherers (485) 17:no 21:no 22:no 24:yes 25:yes
flexible constructor (513) 17:no 21:no 22:no 24:no 25:yes
compact source file (512) 17:no 21:no 22:no 24:no 25:yesEach row turns to yes in the release that finalized the feature. Before that release the feature is either missing or a preview, and a preview feature without --enable-preview is rejected with a message such as Gatherers is a preview API and is disabled by default.
Example 3: runtime facts that changed between 17 and 25
Some changes are visible only at run time. This program checks the default charset (UTF-8 since Java 18), the Security Manager (permanently disabled in Java 24), and strong encapsulation of JDK internals (Java 17).
import java.nio.charset.Charset;
public class RuntimeChecks {
public static void main(String[] args) throws Exception {
System.out.println("feature release >= 25: " + (Runtime.version().feature() >= 25));
System.out.println("default charset: " + Charset.defaultCharset());
try {
System.setSecurityManager(null);
System.out.println("setSecurityManager: allowed");
} catch (UnsupportedOperationException e) {
System.out.println("setSecurityManager: UnsupportedOperationException");
}
var field = String.class.getDeclaredField("value");
try {
field.setAccessible(true);
System.out.println("String.value accessible");
} catch (RuntimeException e) {
System.out.println("String.value: " + e.getClass().getSimpleName());
}
}
}feature release >= 25: true
default charset: UTF-8
setSecurityManager: UnsupportedOperationException
String.value: InaccessibleObjectExceptionCharset.defaultCharset() reports UTF-8 even on a machine whose locale encoding is ASCII; the locale encoding is still available as the native.encoding system property. setAccessible on a private field of java.base fails unless the module is opened with --add-opens java.base/java.lang=ALL-UNNAMED.
Migrating from 17 to 21 to 25
Each item names what changed and what to do. The JVM flag results were checked on JDK 25.
| Change | Release | Effect | Action |
|---|---|---|---|
| Strong encapsulation (JEP 403) | 17 | --illegal-access is gone; deep reflection into JDK internals throws InaccessibleObjectException |
add targeted --add-opens, or upgrade the library that needs it |
| Default charset UTF-8 (JEP 400) | 18 | FileReader, new String(bytes) and similar use UTF-8 regardless of locale |
pass a charset explicitly; -Dfile.encoding=COMPAT restores the old behavior |
| Finalization deprecated for removal (JEP 421) | 18 | finalize still runs but is warned about; --finalization=disabled turns it off |
replace with Cleaner or try-with-resources, then test with --finalization=disabled |
| Biased locking removed | 18 | -XX:+UseBiasedLocking gives Unrecognized VM option and the JVM does not start |
delete the flag |
| Security Manager deprecated (JEP 411), then disabled (JEP 486) | 17, 24 | on 25, -Djava.security.manager=allow stops the JVM at startup and System.setSecurityManager throws UnsupportedOperationException |
remove Security Manager code and flags |
| Dynamic agent loading warning (JEP 451) | 21 | agents attached at run time print a warning | start agents with -javaagent, or set -XX:+EnableDynamicAgentLoading |
Virtual threads pinned by synchronized |
21 to 23 | blocking inside synchronized pins the carrier thread |
fixed in 24 (JEP 491); on 21 use ReentrantLock in hot paths |
ZGenerational |
21, 23, 24 | 21: opt-in; 23: generational is default; 24: non-generational removed, the flag is ignored with a warning | drop -XX:+ZGenerational; plain -XX:+UseZGC is generational |
| JNI restriction warning (JEP 472) | 24 | native code loaded through JNI prints a warning | add --enable-native-access=ALL-UNNAMED or a module name |
sun.misc.Unsafe memory access deprecated (JEPs 471, 498) |
23, 24 | warnings on first use | move to VarHandle or the FFM API |
| 32-bit x86 port removed (JEP 503) | 25 | no 32-bit x86 builds | use a 64-bit JDK |
The JDK 25 launcher output for two of the removed flags:
$ java -XX:+UseBiasedLocking -version
Unrecognized VM option 'UseBiasedLocking'
Error: Could not create the Java Virtual Machine.
$ java -XX:+ZGenerational -version
OpenJDK 64-Bit Server VM warning: Ignoring option ZGenerational; support was removed in 24.0A migration usually goes in this order: build on the new JDK with --release of the old one, run tests, remove failing flags, fix reflection errors with --add-opens or library upgrades, then raise --release and start using new language features.
Interview points
- LTS releases are 17, 21, and 25. Most production upgrades go from one LTS to the next.
- A feature is final in one release and usually previewed in earlier ones under different JEP numbers; virtual threads were previews in 19 and 20 and final in 21.
- Java 21 finalized virtual threads, pattern
switch, record patterns, and sequenced collections. Java 22 finalized_and the FFM API. Java 24 finalized gatherers and removedsynchronizedpinning. Java 25 finalized compact source files, module imports, flexible constructor bodies, and scoped values. - Structured concurrency and primitive patterns are still previews in Java 25.
-XX:+UseBiasedLockingstops the JVM from starting on 18 and later;-XX:+ZGenerationalis ignored with a warning on 24 and later.- The Security Manager cannot be enabled on 24 and later.
- UTF-8 is the default charset since 18;
native.encodingholds the platform encoding.
Summary
- Java 17: sealed classes, strong encapsulation, Security Manager deprecated.
- Java 18: UTF-8 default, finalization deprecated for removal, biased locking code removed.
- Java 21: virtual threads, pattern
switch, record patterns, sequenced collections, generational ZGC. - Java 22: unnamed variables and patterns, FFM API, G1 region pinning.
- Java 23: generational ZGC by default,
Unsafememory access deprecated, Markdown doc comments. - Java 24: gatherers, no pinning in
synchronized, Security Manager disabled, AOT class loading, non-generational ZGC removed. - Java 25: compact source files, module imports, flexible constructor bodies, scoped values, compact object headers as a product option, generational Shenandoah as a product mode.
