Guide contents

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).

Java
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));
}
Output
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: 100

The 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.

Java
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());
        }
    }
}
Output
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:yes

Each 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).

Java
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());
        }
    }
}
Output
feature release >= 25: true
default charset: UTF-8
setSecurityManager: UnsupportedOperationException
String.value: InaccessibleObjectException

Charset.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:

Text
$ 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.0

A 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 removed synchronized pinning. 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:+UseBiasedLocking stops the JVM from starting on 18 and later; -XX:+ZGenerational is 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.encoding holds 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, Unsafe memory 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.