What's New in Java 17

Published 2025-10-01 21:00 Updated 2026-08-01 21:00 7630 words 39 min read

issyuu avatar

issyuu

Quiet waters, shimmering Lakeheart Retreat; thoughts and daily musings woven by the peaceful waterside.

All 14 JEPs of Java 17 (LTS), explained with runnable samples and their real output. The focus is on sealed classes, pattern matching for switch, the new random number API and strong encapsulation. It also covers the Java 12-16 features you get when moving from Java 11, and each feature's later changes up to JDK 26.

Introduction

Java 17 is the LTS (long-term support) release that shipped in September 2021. It is the LTS after Java 11, and Spring Boot 3 made it the minimum version, so many projects still use it as their baseline. Oracle's Premier Support for Java 17 ends in September 2026, which makes now a good time to take stock of projects that are still on 17.

This article walks through the 14 JEPs of Java 17 by running them. Every sample is a single file with no external libraries, so you can copy it and run it with the java command as is.

Many of the Java 17 JEPs are "quiet" ones, such as removals and deprecations. But if you upgrade from Java 11, you also get everything that became standard in Java 12-16, such as records and text blocks. So before the main part, we briefly review those features. Each feature also ends with a "Since then" note that covers what happened up to JDK 26 (GA in March 2026).

Test environment and how to run the samples

  • JDK: OpenJDK 17 (17.0.20)
  • OS / CPU: macOS (Apple Silicon, 8 cores)
  • The samples are launched directly from source (JEP 330, available since Java 11). No compilation needed.
java SealedClassesDemo.java                                 # standard features
java --enable-preview --source 17 SwitchPatternDemo.java    # preview features
  • The samples are also on GitHub at smile-lxy/java-new-features. Their comments are written in Japanese, Chinese and English; this article shows only the English comments.
  • The first comment of each sample (// Run:) gives the command to run it.
  • Times and hash codes in the output (such as @6b1274d2) vary by environment and from run to run.

Overview: the 14 JEPs of Java 17

JEPFeatureStatus in Java 17Since then (up to JDK 26)
306Always-strict floating-point semanticsStandard—
356Enhanced pseudo-random number generatorsStandardJDK 19 added Random.from()
382New macOS rendering pipelineAdded (OpenGL still the default)Metal became the default in JDK 19
391macOS/AArch64 portStandard—
398Applet API deprecated for removalDeprecated for removalRemoved in JDK 26
403Strong encapsulation of JDK internalsStandardMemory access through sun.misc.Unsafe is being restricted too
406Pattern Matching for switchPreviewStandard in JDK 21
407RMI Activation removedRemoved—
409Sealed ClassesStandard—
410Experimental AOT and JIT compiler removedRemovedAOT came back in another form through Project Leyden
411Security Manager deprecated for removalDeprecated for removalPermanently disabled in JDK 24
412Foreign Function & Memory APIIncubatorStandard in JDK 22
414Vector APISecond incubatorStill an incubator in JDK 26 (eleventh)
415Context-specific deserialization filtersStandard—

They fall into four groups.

flowchart LR
    J17["Java 17"] --> L["Language"]
    J17 --> A["Libraries"]
    J17 --> S["Security / upkeep"]
    J17 --> P["Platforms"]
    L --> L1["Sealed Classes (409)"]
    L --> L2["Patterns in switch (406)"]
    L --> L3["strictfp cleanup (306)"]
    A --> A1["Random API (356)"]
    A --> A2["FFM API (412)"]
    A --> A3["Vector API (414)"]
    S --> S1["Strong encapsulation (403)"]
    S --> S2["Deserialization filters (415)"]
    S --> S3["Removals (398 / 407 / 410 / 411)"]
    P --> P1["macOS/AArch64 (391)"]
    P --> P2["Metal rendering (382)"]
    classDef star fill:#fde68a,stroke:#b45309,color:#78350f
    class L1,A1,S1 star

We go in this order: a review of Java 12-16 → language → libraries → security and upkeep → platforms. The three features that affect everyday code most (in yellow) get the most detail.

Background: what became standard in Java 12-16

When you move from Java 11 to 17, you also get everything that became standard in 12-16. Sealed classes and pattern matching for switch in Java 17 build on these features, so let's go over them first.

VersionMain features
Java 12Collectors.teeing, String.indent / transform
Java 14Switch expressions (JEP 361), helpful NullPointerExceptions (JEP 358)
Java 15Text blocks (JEP 378), ZGC and Shenandoah become production-ready
Java 16Records (JEP 395), pattern matching for instanceof (JEP 394), Stream.toList(), jpackage
flowchart TB
    R["record (16)<br/>declares the shape of data"] --> S["sealed (17)<br/>closes the set of types"]
    I["instanceof patterns (16)<br/>test a type and extract"] --> P["switch patterns (17 preview)<br/>branch by type"]
    W["switch expressions (14)<br/>a switch that returns a value"] --> P
    S --> P
    P --> D["Data-oriented programming<br/>(complete in Java 21)"]

Records declare the shape of the data, sealed types declare which kinds exist, and patterns handle each kind separately. Record patterns in Java 21 complete this story, but more than half of it is already in place by Java 17.

// Java 12-16: features that became standard between Java 11 and Java 17
// Run: java Java12To16Demo.java

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Java12To16Demo {

    // Records (JEP 395, Java 16): constructor, accessors, equals, hashCode and toString are generated
    record Order(String id, int amount) {
        // Validate the values in a compact constructor
        Order {
            if (amount < 0) throw new IllegalArgumentException("amount < 0");
        }
    }

    record Customer(String name, Address address) {}
    record Address(String city) {}

    public static void main(String[] args) {
        // Text blocks (JEP 378, Java 15)
        String json = """
                {
                  "id": "A-1",
                  "amount": 300
                }
                """;
        System.out.print("--- text block ---\n" + json);

        // Switch expressions (JEP 361, Java 14): they return a value and need no break
        for (int day : new int[] {1, 6, 8}) {
            String type = switch (day) {
                case 1, 2, 3, 4, 5 -> "weekday";
                case 6, 7 -> "weekend";
                default -> {
                    String message = "invalid day " + day;
                    yield message;
                }
            };
            System.out.println("day " + day + " -> " + type);
        }

        // Pattern matching for instanceof (JEP 394, Java 16): no cast needed
        Object o = new Order("A-1", 300);
        if (o instanceof Order order && order.amount() > 100) {
            System.out.println("large order: " + order);
        }
        System.out.println("equals by value: " + o.equals(new Order("A-1", 300)));

        // Stream.toList() (Java 16) and Collectors.teeing (Java 12)
        List<Integer> numbers = Stream.of(3, 1, 4, 1, 5, 9).toList();
        double average = numbers.stream().collect(Collectors.teeing(
                Collectors.summingInt(i -> i), Collectors.counting(), (sum, n) -> (double) sum / n));
        System.out.println("numbers = " + numbers + ", average = " + average);

        // Helpful NullPointerExceptions (JEP 358, added in Java 14, on by default since Java 15)
        Customer customer = new Customer("Alice", null);
        try {
            System.out.println(customer.address().city());
        } catch (NullPointerException e) {
            System.out.println("NPE: " + e.getMessage());
        }
    }
}
--- text block ---
{
  "id": "A-1",
  "amount": 300
}
day 1 -> weekday
day 6 -> weekend
day 8 -> invalid day 8
large order: Order[id=A-1, amount=300]
equals by value: true
numbers = [3, 1, 4, 1, 5, 9], average = 3.8333333333333335
NPE: Cannot invoke "Java12To16Demo$Address.city()" because the return value of "Java12To16Demo$Customer.address()" is null

Look at the NullPointerException message at the end. Up to Java 11 it was just null; now it tells you which expression was null. That alone makes the upgrade worth it.

Language

Sealed Classes (JEP 409)

You can now restrict which types may extend a class or interface. Add sealed and list the allowed types in permits.

flowchart TB
    S["sealed interface Shape<br/>permits Circle, Square, Polygon"] --> C["record Circle<br/>(implicitly final)"]
    S --> Q["final class Square"]
    S --> P["non-sealed class Polygon"]
    P --> T["class Triangle<br/>(anyone may extend)"]
    S -.->|"compile error"| H["record Hexagon"]
    style H fill:#fecaca,stroke:#b91c1c,color:#7f1d1d

Each permitted subclass must declare one of the following.

ModifierMeaning
finalNo further subclasses (records and enums are implicitly final)
sealedRestrict its own subclasses again with permits
non-sealedLift the restriction; anyone may extend it from here on
// JEP 409: Sealed Classes
// Run: java SealedClassesDemo.java

import java.util.Arrays;
import java.util.List;

public class SealedClassesDemo {

    // Only the three types listed in permits may extend Shape
    sealed interface Shape permits Circle, Square, Polygon {}

    // A record is implicitly final
    record Circle(double radius) implements Shape {}

    // final: no further subclasses
    static final class Square implements Shape {
        final double side;
        Square(double side) { this.side = side; }
    }

    // non-sealed: anyone may extend it from here on
    static non-sealed class Polygon implements Shape {
        final int sides;
        Polygon(int sides) { this.sides = sides; }
    }

    static class Triangle extends Polygon {
        Triangle() { super(3); }
    }

    // The next line does not compile (Shape rejects types not listed in permits)
    // record Hexagon() implements Shape {}

    // switch cannot take patterns yet in Java 17 (still a preview), so branch with instanceof
    static double area(Shape shape) {
        if (shape instanceof Circle c) {
            return Math.PI * c.radius() * c.radius();
        } else if (shape instanceof Square s) {
            return s.side * s.side;
        } else if (shape instanceof Polygon p) {
            // The number of sides alone does not give the area
            return Double.NaN;
        }
        throw new AssertionError("unreachable: Shape is sealed");
    }

    public static void main(String[] args) {
        // Reflection can read the sealing information (APIs added in JDK 17)
        System.out.println("Shape.isSealed()          = " + Shape.class.isSealed());
        System.out.println("permitted subclasses      = " + Arrays.stream(Shape.class.getPermittedSubclasses())
                .map(Class::getSimpleName).toList());
        System.out.println("Polygon.isSealed()        = " + Polygon.class.isSealed());
        System.out.println("Circle is a record        = " + Circle.class.isRecord());
        System.out.println();

        for (Shape s : List.of(new Circle(1), new Square(2), new Triangle())) {
            System.out.printf("%-9s area = %.2f%n", s.getClass().getSimpleName(), area(s));
        }
    }
}
Shape.isSealed()          = true
permitted subclasses      = [Circle, Square, Polygon]
Polygon.isSealed()        = false
Circle is a record        = true

Circle    area = 3.14
Square    area = 4.00
Triangle  area = NaN

If you uncomment record Hexagon() implements Shape {}, compilation fails like this.

error: class is not allowed to extend sealed class: Shape (as it is not listed in its permits clause)
    record Hexagon() implements Shape {}
    ^

You might think, "If I don't want subclasses, I can just use final." But final also stops you from writing your own subclasses. Sealed types let you express domain knowledge such as "there are exactly these three kinds" as a type. And once the compiler knows the set of kinds is closed, a switch can check that every case is handled, as the next section shows.

The types listed in permits must be in the same module (in the same package, for the unnamed module). If the subclasses are in the same file, you can leave out permits.

Pattern Matching for switch (JEP 406, preview)

A case label can now hold a type pattern. This extends the instanceof pattern matching of Java 16 to switch. It is a preview in Java 17, so it needs --enable-preview --source 17.

// JEP 406: Pattern Matching for switch (preview in Java 17)
// Run: java --enable-preview --source 17 SwitchPatternDemo.java

import java.util.Arrays;

public class SwitchPatternDemo {

    sealed interface Shape permits Circle, Square, Rect {}
    record Circle(double radius) implements Shape {}
    record Square(double side) implements Shape {}
    record Rect(double width, double height) implements Shape {}

    // Before: a chain of instanceof if-else
    static String formatOld(Object o) {
        if (o instanceof Integer i) {
            return "int " + i;
        } else if (o instanceof Long l) {
            return "long " + l;
        } else if (o instanceof String s) {
            return "String \"" + s + "\"";
        }
        return "other";
    }

    // Java 17 (preview): case labels take type patterns, and case null is allowed
    static String formatNew(Object o) {
        return switch (o) {
            case null      -> "null!";
            case Integer i -> "int " + i;
            case Long l    -> "long " + l;
            case String s  -> "String \"" + s + "\"";
            case int[] a   -> "int[] " + Arrays.toString(a);
            default        -> "other";
        };
    }

    // A switch without case null still throws NullPointerException on null, as before
    static String withoutNullCase(Object o) {
        return switch (o) {
            case String s -> "String";
            default       -> "other";
        };
    }

    // Guarded patterns: Java 17 adds the condition with && (JDK 19 changed it to when)
    static String size(Shape shape) {
        return switch (shape) {
            case Circle c && c.radius() > 10 -> "large circle";
            case Circle c                    -> "small circle";
            case Square s                    -> "square " + s.side();
            case Rect r                      -> "rect " + r.width() + "x" + r.height();
            // Shape is sealed, so no default is needed (the compiler checks exhaustiveness)
        };
    }

    public static void main(String[] args) {
        Object[] values = {42, 42L, "hello", new int[] {1, 2}, 3.14, null};
        System.out.printf("%-16s | %s%n", "formatNew", "formatOld");
        for (Object v : values) {
            System.out.printf("%-16s | %s%n", formatNew(v), formatOld(v));
        }
        try {
            withoutNullCase(null);
        } catch (NullPointerException e) {
            System.out.println("withoutNullCase(null) -> NullPointerException");
        }
        System.out.println();
        for (Shape s : new Shape[] {new Circle(20), new Circle(1), new Square(2), new Rect(2, 3)}) {
            System.out.println(s + " -> " + size(s));
        }
    }
}
Note: SwitchPatternDemo.java uses preview features of Java SE 17.
Note: Recompile with -Xlint:preview for details.
formatNew        | formatOld
int 42           | int 42
long 42          | long 42
String "hello"   | String "hello"
int[] [1, 2]     | other
other            | other
null!            | other
withoutNullCase(null) -> NullPointerException

Circle[radius=20.0] -> large circle
Circle[radius=1.0] -> small circle
Square[side=2.0] -> square 2.0
Rect[width=2.0, height=3.0] -> rect 2.0x3.0

There are three points.

  1. Type patterns: case Integer i tests the type and binds the variable in one step. It reads better than a chain of if-else and is easier for the compiler to optimize.
  2. case null: a switch used to throw NullPointerException whenever it got null. With case null, null becomes just another case. Without it, the switch still throws, as before.
  3. Exhaustiveness checking: Shape is sealed, so covering Circle, Square and Rect is enough; no default is needed. If you add a fourth type to Shape, this switch stops compiling, so you can't miss a place to update.

In Java 17, a guarded pattern uses &&, as in case Circle c && c.radius() > 10. JDK 19 replaced this syntax with when, so this sample does not compile on JDK 21.

SwitchPatternDemo.java:48: error: : or -> expected
            case Circle c && c.radius() > 10 -> "large circle";
                         ^

Pattern matching for switch went through four previews before it became standard.

flowchart TB
    A["JDK 17<br/>1st preview<br/>(406)"] --> B["JDK 18<br/>2nd<br/>(420)"]
    B --> C["JDK 19<br/>3rd (427)<br/>&& → when"]
    C --> D["JDK 20<br/>4th<br/>(433)"]
    D --> E["JDK 21<br/>standard (441)<br/>+ record patterns"]
    style E fill:#bbf7d0,stroke:#15803d,color:#14532d

Since then: it became standard in JDK 21 (JEP 441). Record patterns (JEP 440) became standard at the same time, so you can deconstruct a record while branching, as in case Circle(double r) when r > 10. Primitive type patterns such as case int i have been a preview since JDK 23 and are in their fourth preview in JDK 26 (JEP 530).

Always-strict floating-point semantics (JEP 306)

Floating-point arithmetic is now always strictly IEEE 754, with or without strictfp.

The old x87 FPU computed internally with 80-bit extended precision. Computing strictly in 64-bit IEEE 754 was slow on it, so Java 1.2 gave floating-point two meanings: "a little loose by default, strict only with strictfp". Modern CPUs (x86 since SSE2, and ARM) are not slower with strict arithmetic, so JEP 306 went back to the "always strict" rule from before Java 1.2.

// JEP 306: Restore Always-Strict Floating-Point Semantics
// Run: java StrictFpDemo.java

public class StrictFpDemo {

    // Up to Java 16, strictfp could change how intermediate results were handled
    static strictfp double withStrictfp(double x) {
        return x * 10 / 10;
    }

    // Since Java 17, every expression is evaluated strictly (IEEE 754), with or without strictfp
    static double withoutStrictfp(double x) {
        return x * 10 / 10;
    }

    public static void main(String[] args) {
        // x * 10 exceeds the largest double (about 1.8e308), so it becomes Infinity midway
        double x = 1e308;
        System.out.println("with strictfp    : " + withStrictfp(x));
        System.out.println("without strictfp : " + withoutStrictfp(x));
        System.out.println("0.1 + 0.2        : " + (0.1 + 0.2));
    }
}
StrictFpDemo.java:7: warning: [strictfp] as of release 17, all floating-point expressions are evaluated strictly and 'strictfp' is not required
    static strictfp double withStrictfp(double x) {
                           ^
1 warning
with strictfp    : Infinity
without strictfp : Infinity
0.1 + 0.2        : 0.30000000000000004

strictfp no longer means anything, so javac warns about it. Both methods give the same result: x * 10 overflows to Infinity along the way. On the old extended-precision hardware, the method without strictfp could come back to 1.0E308.

In practice this changes almost nothing. If your code uses strictfp, you can delete it.

Enhanced pseudo-random number generators (JEP 356)

Random number generators (PRNGs) now share a common interface, RandomGenerator, and you can pick the algorithm that fits your use.

One interface: RandomGenerator

Java already had Random, ThreadLocalRandom and SplittableRandom, but they had no common interface, which made it hard to swap algorithms. In Java 17 they all implement RandomGenerator, and sub-interfaces describe what each kind of generator can do.

flowchart TB
    RG["RandomGenerator<br/>nextInt / nextLong / nextDouble …"] --> ST["StreamableGenerator<br/>a Stream of independent generators"]
    ST --> SP["SplittableGenerator<br/>split()"]
    ST --> JU["JumpableGenerator<br/>jump()"]
    JU --> LE["LeapableGenerator<br/>leap()"]
    LE --> AJ["ArbitrarilyJumpable<br/>Generator"]
    SP --> LXM["LXM family (L64X128MixRandom, …)<br/>SplittableRandom"]
    LE --> XO["Xoshiro256PlusPlus<br/>Xoroshiro128PlusPlus"]
    RG --> LG["Random / SecureRandom<br/>ThreadLocalRandom"]
// JEP 356: Enhanced Pseudo-Random Number Generators
// Run: java RandomGeneratorDemo.java

import java.util.Comparator;
import java.util.Random;
import java.util.random.RandomGenerator;
import java.util.random.RandomGenerator.SplittableGenerator;
import java.util.random.RandomGenerator.JumpableGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomGeneratorDemo {

    public static void main(String[] args) {
        // The available algorithms (group, state bits, properties)
        System.out.println("--- algorithms ---");
        RandomGeneratorFactory.all()
                .sorted(Comparator.comparing(RandomGeneratorFactory<RandomGenerator>::group)
                        .thenComparing(RandomGeneratorFactory::name))
                .forEach(f -> System.out.printf("%-10s %-22s %9s  %s%n", f.group(), f.name(),
                        f.stateBits() == Integer.MAX_VALUE ? "-" : f.stateBits() + " bits",
                        f.isSplittable() ? "splittable" : f.isJumpable() ? "jumpable" : f.isStreamable() ? "streamable" : ""));

        // The existing classes now implement the common RandomGenerator interface
        System.out.println();
        System.out.println("--- one interface ---");
        // The first is the old Random, the second is picked by name, the third is the default algorithm
        RandomGenerator[] generators = {
                new Random(42),
                RandomGenerator.of("L64X128MixRandom"),
                RandomGenerator.getDefault(),
        };
        for (RandomGenerator g : generators) {
            System.out.printf("%-34s nextInt(100) = %d%n", g.getClass().getName(), g.nextInt(100));
        }

        // The same seed gives the same sequence (reproducible)
        System.out.println();
        System.out.println("--- seeds ---");
        RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Xoshiro256PlusPlus");
        System.out.println("seed 2021 run 1: " + factory.create(2021).ints(5, 0, 100).boxed().toList());
        System.out.println("seed 2021 run 2: " + factory.create(2021).ints(5, 0, 100).boxed().toList());

        // split(): create statistically independent child generators (for parallel work)
        System.out.println();
        System.out.println("--- split / jump ---");
        SplittableGenerator parent = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(7);
        System.out.println("child 1: " + parent.split().ints(3, 0, 100).boxed().toList());
        System.out.println("child 2: " + parent.split().ints(3, 0, 100).boxed().toList());

        // jump(): advance the state by 2^128 steps at once (non-overlapping subsequences)
        JumpableGenerator jumpable = (JumpableGenerator) RandomGeneratorFactory.of("Xoshiro256PlusPlus").create(7);
        JumpableGenerator copy = jumpable.copy();
        jumpable.jump();
        System.out.println("before jump: " + copy.nextInt(100) + ", after jump: " + jumpable.nextInt(100)
                + " (jump distance = 2^" + Math.round(Math.log(jumpable.jumpDistance()) / Math.log(2)) + ")");
    }
}
--- algorithms ---
LXM        L128X1024MixRandom     1152 bits  splittable
LXM        L128X128MixRandom       256 bits  splittable
LXM        L128X256MixRandom       384 bits  splittable
LXM        L32X64MixRandom          96 bits  splittable
LXM        L64X1024MixRandom      1088 bits  splittable
LXM        L64X128MixRandom        192 bits  splittable
LXM        L64X128StarStarRandom   192 bits  splittable
LXM        L64X256MixRandom        320 bits  splittable
Legacy     Random                   48 bits  
Legacy     SecureRandom                   -  
Legacy     SplittableRandom         64 bits  splittable
Xoroshiro  Xoroshiro128PlusPlus    128 bits  jumpable
Xoshiro    Xoshiro256PlusPlus      256 bits  jumpable

--- one interface ---
java.util.Random                   nextInt(100) = 30
jdk.random.L64X128MixRandom        nextInt(100) = 61
jdk.random.L32X64MixRandom         nextInt(100) = 72

--- seeds ---
seed 2021 run 1: [83, 80, 16, 88, 25]
seed 2021 run 2: [83, 80, 16, 88, 25]

--- split / jump ---
child 1: [62, 27, 16]
child 2: [38, 52, 12]
before jump: 26, after jump: 73 (jump distance = 2^128)

A rough guide to choosing an algorithm:

UseChoice
No special needsRandomGenerator.getDefault() (currently L32X64MixRandom)
Parallel work (streams, Fork/Join)split() an LXM generator and give one to each thread
Non-overlapping subsequences for simulationsjump() / leap() of the Xoshiro family
Cryptography, tokensKeep using SecureRandom

The existing Random still works, and you can pass new Random() to any method that takes a RandomGenerator.

Measured: the cost of sharing one Random across threads

java.util.Random is thread-safe, but it updates its internal state (the seed) with a CAS on an AtomicLong. When several threads share one Random, they fight over that CAS. We called nextLong() 2,000,000 times on each of 8 threads and compared the times.

// JEP 356: Enhanced Pseudo-Random Number Generators - sharing one Random across threads vs split()
// Run: java RandomContentionDemo.java

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.IntFunction;
import java.util.random.RandomGenerator;
import java.util.random.RandomGenerator.SplittableGenerator;
import java.util.random.RandomGeneratorFactory;

public class RandomContentionDemo {

    static final int THREADS = 8;
    static final int PER_THREAD = 2_000_000;
    static volatile long sink;

    // Call nextLong() PER_THREAD times on each of THREADS threads; return the elapsed time (ms)
    static long run(IntFunction<RandomGenerator> generatorForThread) throws InterruptedException {
        Thread[] threads = new Thread[THREADS];
        long start = System.nanoTime();
        for (int t = 0; t < THREADS; t++) {
            int id = t;
            threads[t] = new Thread(() -> {
                // Get the generator inside the thread (needed for ThreadLocalRandom.current())
                RandomGenerator g = generatorForThread.apply(id);
                long sum = 0;
                for (int i = 0; i < PER_THREAD; i++) sum += g.nextLong();
                sink += sum;
            });
            threads[t].start();
        }
        for (Thread th : threads) th.join();
        return (System.nanoTime() - start) / 1_000_000;
    }

    public static void main(String[] args) throws InterruptedException {
        Random shared = new Random(1);
        SplittableGenerator root = (SplittableGenerator) RandomGeneratorFactory.of("L64X128MixRandom").create(1);
        SplittableGenerator[] children = new SplittableGenerator[THREADS];
        for (int t = 0; t < THREADS; t++) children[t] = root.split();

        System.out.printf("%d threads x %,d nextLong() calls%n%n", THREADS, PER_THREAD);
        // The first round warms up the JIT; show the next three rounds
        for (int round = 0; round <= 3; round++) {
            // (1) All threads share one Random (they fight over its AtomicLong with CAS)
            long a = run(t -> shared);
            // (2) ThreadLocalRandom (per-thread state)
            long b = run(t -> ThreadLocalRandom.current());
            // (3) One L64X128MixRandom from split() per thread
            long c = run(t -> children[t]);
            if (round > 0) {
                System.out.printf("round %d: shared Random %5d ms | ThreadLocalRandom %4d ms | split() %4d ms%n", round, a, b, c);
            }
        }
    }
}
8 threads x 2,000,000 nextLong() calls

round 1: shared Random  3197 ms | ThreadLocalRandom   22 ms | split()   39 ms
round 2: shared Random  2545 ms | ThreadLocalRandom   40 ms | split()   72 ms
round 3: shared Random  1226 ms | ThreadLocalRandom   27 ms | split()   41 ms

8 threads x 2,000,000 nextLong() calls (measured, average of 3 rounds, axis in s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 500millisecond
    section Random (shared)
    2323 ms :crit, 0, 2323
    section ThreadLocalRandom
    30 ms :active, 0, 30
    section split()
    51 ms :active, 0, 51

The shared Random is tens of times slower than the generators that keep per-thread state. Every failed CAS has to be retried, so adding threads does not help; it makes things worse.

ThreadLocalRandom is the fastest because it keeps its state directly in the Thread object, and the JIT compiler treats it specially. If parallel work must be reproducible (same seed, same result), use split(), because ThreadLocalRandom does not let you set a seed.

Since then: JDK 19 added Random.from(RandomGenerator), so you can hand a new algorithm to older APIs that ask for a Random.

Strong encapsulation of JDK internals (JEP 403)

Reflection can no longer reach the JDK's internal APIs (sun.* packages, and private members of java.*). This is the last step of the tightening that started with the module system in Java 9.

flowchart TB
    A["JDK 9-15<br/>--illegal-access=permit<br/>(allowed, with a warning)"] --> B["JDK 16 (JEP 396)<br/>default becomes deny"]
    B --> C["JDK 17 (JEP 403)<br/>--illegal-access removed<br/>(ignored if given)"]
    style C fill:#fecaca,stroke:#b91c1c,color:#7f1d1d
// JEP 403: Strongly Encapsulate JDK Internals
// Run: java EncapsulationDemo.java

import java.lang.reflect.Field;

public class EncapsulationDemo {

    public static void main(String[] args) throws Exception {
        // (1) Access the private field value of java.lang.String with reflection
        Field value = String.class.getDeclaredField("value");
        try {
            value.setAccessible(true);
            byte[] bytes = (byte[]) value.get("hello");
            System.out.println("(1) String.value opened: " + bytes.length + " bytes");
        } catch (RuntimeException e) {
            System.out.println("(1) " + e.getClass().getSimpleName() + ": " + e.getMessage());
        }

        // (2) Classes in JDK internal packages (sun.nio.ch) cannot be opened with reflection either
        try {
            Class<?> util = Class.forName("sun.nio.ch.Util");
            util.getDeclaredMethod("getTemporaryDirectBuffer", int.class).setAccessible(true);
            System.out.println("(2) sun.nio.ch.Util opened");
        } catch (RuntimeException e) {
            System.out.println("(2) " + e.getClass().getSimpleName());
        }

        // (3) sun.misc.Unsafe lives in the jdk.unsupported module and still works in Java 17 (an exception)
        Field theUnsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        System.out.println("(3) sun.misc.Unsafe: " + theUnsafe.get(null).getClass().getName());

        // (4) Does java.base open the java.lang package to the unnamed module?
        Module base = String.class.getModule();
        System.out.println("(4) java.lang opened to us: " + base.isOpen("java.lang", EncapsulationDemo.class.getModule()));
    }
}
(1) InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @6b1274d2
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: false

If you really need access, open the package explicitly, one package at a time, with --add-opens. --illegal-access=permit, the escape hatch up to Java 16, is ignored in Java 17.

java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationDemo.java
(1) String.value opened: 5 bytes
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: true
java --illegal-access=permit EncapsulationDemo.java
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=permit; support was removed in 17.0
(1) InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @6b1274d2
(2) InaccessibleObjectException
(3) sun.misc.Unsafe: sun.misc.Unsafe
(4) java.lang opened to us: false

Old versions of some libraries (Lombok, Mockito, some ORMs and serializers, and so on) may depend on internal APIs. If you see InaccessibleObjectException after moving to Java 17, the right fix is to update the library first. Treat --add-opens as a temporary workaround for when you cannot update.

Some internal APIs, such as sun.misc.Unsafe, are still exported from the jdk.unsupported module ((3) in the output). They have no replacement yet, so they stay available during the transition.

Since then: with VarHandle and the FFM API available as replacements, sun.misc.Unsafe is being restricted too. Its memory-access methods were deprecated for removal in JDK 23 (JEP 471), and since JDK 24 using them prints a warning (JEP 498). Since JDK 24, using JNI also prints a warning (JEP 472), and in JDK 26, changing a final field through reflection prints a warning as well (JEP 500).

Context-specific deserialization filters (JEP 415)

Java deserialization (ObjectInputStream) instantiates whatever classes the incoming bytes name. Java 9 added filters (JEP 290) to defend against "deserialization attacks" that abuse this. But only one filter could be set for the whole JVM, which made per-context control, such as "only these classes for this connection", hard.

Java 17 lets you register a filter factory. The factory is called whenever a stream is created and whenever a stream gets its own filter, and it decides which filter is actually used.

sequenceDiagram
    participant App as App
    participant OIS as ObjectInputStream
    participant F as Filter factory
    App->>OIS: new ObjectInputStream(...)
    OIS->>F: apply(null, null)
    F-->>OIS: base rule
    App->>OIS: setObjectInputFilter(allowlist)
    OIS->>F: apply(base rule, allowlist)
    F-->>OIS: merged filter
    App->>OIS: readObject()
    Note over OIS: checks each class<br/>with the merged filter
// JEP 415: Context-Specific Deserialization Filters
// Run: java DeserializationFilterDemo.java

import java.io.*;
import java.util.ArrayList;
import java.util.function.BinaryOperator;

public class DeserializationFilterDemo {

    record Order(String id, int amount) implements Serializable {}

    // An example of an "unexpected class" that an attacker might send
    static class Gadget implements Serializable {}

    static byte[] serialize(Object o) throws IOException {
        var out = new ByteArrayOutputStream();
        try (var oos = new ObjectOutputStream(out)) {
            oos.writeObject(o);
        }
        return out.toByteArray();
    }

    static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
            return ois.readObject();
        }
    }

    public static void main(String[] args) throws Exception {
        // JVM-wide base rule: limit the nesting depth and array length
        ObjectInputFilter base = ObjectInputFilter.Config.createFilter("maxdepth=10;maxarray=1000");

        // New in Java 17: a filter factory decides which filter each stream uses
        BinaryOperator<ObjectInputFilter> factory = (current, requested) -> {
            if (requested == null) {
                // When the stream is created: apply the base rule
                return base;
            }
            // When setObjectInputFilter() is called: merge the base and the specific rule, and reject if both are undecided
            return ObjectInputFilter.rejectUndecidedClass(ObjectInputFilter.merge(requested, current));
        };
        ObjectInputFilter.Config.setSerialFilterFactory(factory);

        // An allowlist (only DeserializationFilterDemo$Order and part of java.lang)
        ObjectInputFilter allowOrders = ObjectInputFilter.Config.createFilter(
                "DeserializationFilterDemo$Order;java.lang.String;java.lang.Number;java.lang.Integer");

        Object[] inputs = {new Order("A-1", 300), new Gadget(), new int[5000]};
        for (Object input : inputs) {
            byte[] bytes = serialize(input);
            try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
                ois.setObjectInputFilter(allowOrders);
                Object result = ois.readObject();
                System.out.println("ACCEPTED : " + result);
            } catch (InvalidClassException e) {
                System.out.println("REJECTED : " + input.getClass().getSimpleName() + " (" + e.getMessage() + ")");
            }
        }

        // The base rule (maxarray=1000) also applies to streams that never call setObjectInputFilter()
        System.out.println();
        for (Object input : new Object[] {new ArrayList<>(java.util.List.of(1, 2, 3)), new int[5000]}) {
            try {
                System.out.println("ACCEPTED : " + input.getClass().getSimpleName() + " -> " + deserialize(serialize(input)).getClass().getSimpleName());
            } catch (InvalidClassException e) {
                System.out.println("REJECTED : " + input.getClass().getSimpleName() + " (" + e.getMessage() + ")");
            }
        }
    }
}
ACCEPTED : Order[id=A-1, amount=300]
REJECTED : Gadget (filter status: REJECTED)
REJECTED : int[] (filter status: REJECTED)

ACCEPTED : ArrayList -> ArrayList
REJECTED : int[] (filter status: REJECTED)

Gadget, which is not on the allowlist, is rejected. Even on a stream without an allowlist, the base rule (arrays up to 1000 elements) rejects the large array. The point is that the application can make everything safer in one place, even when a library never calls setObjectInputFilter() itself.

Because this matters so much, it was also backported to update releases of Java 8 and 11. Still, the real fix is to not accept untrusted data through Java serialization at all. If you can use a format such as JSON, choose it.

Incubator APIs

An incubator API is an experimental API one step before a preview. It lives in a jdk.incubator.* package, and you must add it explicitly with --add-modules. The API may change a lot in the next release.

Foreign Function & Memory API (JEP 412, incubator)

An API for calling C libraries and safely handling memory outside the heap without JNI. JEP 412 in Java 17 merges the Foreign Memory Access API (JEP 370 and others, since Java 14) and the Foreign Linker API (JEP 389) into one.

// JEP 412: Foreign Function & Memory API (incubator in Java 17)
// Run: java --add-modules jdk.incubator.foreign --enable-native-access=ALL-UNNAMED ForeignMemoryDemo.java

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.MemoryAccess;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.ResourceScope;

public class ForeignMemoryDemo {

    public static void main(String[] args) throws Throwable {
        // (1) Call strlen from the C standard library without JNI
        MethodHandle strlen = CLinker.getInstance().downcallHandle(
                CLinker.systemLookup().lookup("strlen").orElseThrow(),
                MethodType.methodType(long.class, MemoryAddress.class),
                FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER));

        // Closing the ResourceScope frees all the native memory allocated in it
        try (ResourceScope scope = ResourceScope.newConfinedScope()) {
            MemorySegment cString = CLinker.toCString("Hello, Java 17!", scope);
            long length = (long) strlen.invokeExact(cString.address());
            System.out.println("strlen(\"Hello, Java 17!\") = " + length);

            // (2) Allocate room for five ints off-heap, then write and read them
            MemorySegment ints = MemorySegment.allocateNative(5 * 4, scope);
            for (int i = 0; i < 5; i++) {
                MemoryAccess.setIntAtIndex(ints, i, i * i);
            }
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < 5; i++) {
                sb.append(MemoryAccess.getIntAtIndex(ints, i)).append(' ');
            }
            System.out.println("off-heap ints = " + sb.toString().trim() + " (" + ints.byteSize() + " bytes)");

            // (3) An out-of-bounds access throws an exception instead of crashing the JVM
            try {
                MemoryAccess.setIntAtIndex(ints, 5, 25);
            } catch (IndexOutOfBoundsException e) {
                System.out.println("index 5 -> " + e.getClass().getSimpleName());
            }
        }
        System.out.println("scope closed: native memory freed");
    }
}
WARNING: Using incubator modules: jdk.incubator.foreign
warning: using incubating module(s): jdk.incubator.foreign
1 warning
strlen("Hello, Java 17!") = 15
off-heap ints = 0 1 4 9 16 (20 bytes)
index 5 -> IndexOutOfBoundsException
scope closed: native memory freed

With JNI, you have to write C code, compile it and ship a shared library. With the FFM API, plain Java code is enough to call strlen. The allocated memory is freed when the ResourceScope closes, and an out-of-bounds access throws an exception instead of crashing the JVM.

Calling native code can break the JVM's safety guarantees, so you must allow it explicitly with --enable-native-access. Without it, the sample fails like this.

Exception in thread "main" java.lang.IllegalCallerException: Illegal native access from: unnamed module @4f638935

Since then: after a second incubator in JDK 18 and previews in JDK 19-21, it became standard in JDK 22 (JEP 454). The package moved to java.lang.foreign, and the API changed a lot (ResourceScope → Arena, CLinker → Linker, and so on). The code above works only on Java 17 and must be rewritten for JDK 22 and later. Also, in the standard API, a missing --enable-native-access only prints a warning and the program keeps running.

Vector API (JEP 414, second incubator)

An API for writing vector computations that use the CPU's SIMD instructions explicitly from Java. One instruction computes several elements (lanes) at once. Here we compute the dot product of two arrays with a plain loop and with the Vector API, and compare them.

// JEP 414: Vector API (second incubator in Java 17)
// Run: java --add-modules jdk.incubator.vector VectorApiDemo.java

import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;

public class VectorApiDemo {

    static final VectorSpecies<Float> SPECIES = FloatVector.SPECIES_PREFERRED;

    // Compute the dot product of two arrays (a[0]*b[0] + a[1]*b[1] + ...) with a plain loop
    static float scalarDot(float[] a, float[] b) {
        float sum = 0;
        for (int i = 0; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    // The same computation with the Vector API, SPECIES.length() elements at a time
    static float vectorDot(float[] a, float[] b) {
        var acc = FloatVector.zero(SPECIES);
        int i = 0;
        int upper = SPECIES.loopBound(a.length);
        for (; i < upper; i += SPECIES.length()) {
            var va = FloatVector.fromArray(SPECIES, a, i);
            var vb = FloatVector.fromArray(SPECIES, b, i);
            // acc += va * vb (fused multiply-add)
            acc = va.fma(vb, acc);
        }
        float sum = acc.reduceLanes(VectorOperators.ADD);
        // Handle the remaining elements one by one (scalar)
        for (; i < a.length; i++) {
            sum += a[i] * b[i];
        }
        return sum;
    }

    public static void main(String[] args) {
        System.out.println("species = " + SPECIES + ", lanes per operation = " + SPECIES.length());

        float[] small = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        System.out.println("dot(small, small): scalar = " + scalarDot(small, small) + ", vector = " + vectorDot(small, small));

        // Time 500 dot products of 1,000,000 elements
        float check = 0;
        float[] a = new float[1_000_000];
        float[] b = new float[1_000_000];
        for (int i = 0; i < a.length; i++) {
            a[i] = (i % 100) / 100f;
            b[i] = (i % 7) / 7f;
        }
        // Let the JIT compile both methods before timing (warm-up)
        float[] tiny = new float[1024];
        for (int r = 0; r < 20_000; r++) {
            check += scalarDot(tiny, tiny) + vectorDot(tiny, tiny);
        }
        for (int round = 1; round <= 3; round++) {
            long t0 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += scalarDot(a, b);
            long t1 = System.nanoTime();
            for (int r = 0; r < 500; r++) check += vectorDot(a, b);
            long t2 = System.nanoTime();
            System.out.printf("round %d: scalar %4d ms | vector %4d ms%n", round, (t1 - t0) / 1_000_000, (t2 - t1) / 1_000_000);
        }
        // Use the result so the computation is not optimized away
        if (check == 42) System.out.println();
    }
}
WARNING: Using incubator modules: jdk.incubator.vector
warning: using incubating module(s): jdk.incubator.vector
1 warning
species = Species[float, 4, S_128_BIT], lanes per operation = 4
dot(small, small): scalar = 385.0, vector = 385.0
round 1: scalar  575 ms | vector  196 ms
round 2: scalar  519 ms | vector  181 ms
round 3: scalar  560 ms | vector  231 ms

Dot product of 1,000,000 elements x 500 (measured, average of 3 rounds, axis in s)

%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
    dateFormat x
    axisFormat %S.%L
    tickInterval 200millisecond
    section Plain loop
    551 ms :crit, 0, 551
    section Vector API
    203 ms :active, 0, 203

In this environment (Apple Silicon), it processes four floats at a time and is about three times as fast as the plain loop.

When we first timed it without a warm-up, the Vector API was slower in 4 out of 5 runs (700-1100 ms, against about 550 ms for the plain loop). The Vector API is only fast once the JIT compiler has removed the FloatVector objects and replaced them with vector instructions. If you time the code before that optimization kicks in, you are just measuring a loop that creates lots of objects. When you measure the Vector API, include a warm-up, and use JMH if you can.

You might ask, "Doesn't the JIT compiler vectorize loops automatically?" But adding floating-point numbers in a different order can change the result slightly, so the JIT cannot vectorize an accumulating loop like a dot product on its own. With the Vector API, the programmer states explicitly that a different order is fine.

Since then: it is waiting for Project Valhalla (value objects), so it is still an incubator in JDK 26, the eleventh (JEP 529).

Removals and deprecations

Java 17 also continued to clean up old features. One sample checks them all.

// JEP 398 / 407 / 410 / 411: deprecations and removals in Java 17
// Run: java RemovalsDemo.java

import java.nio.file.Files;
import java.nio.file.Path;

public class RemovalsDemo {

    static void printDeprecation(String className) {
        try {
            Deprecated d = Class.forName(className).getAnnotation(Deprecated.class);
            if (d == null) {
                System.out.printf("%-34s not deprecated%n", className);
                return;
            }
            System.out.printf("%-34s @Deprecated(since=\"%s\", forRemoval=%s)%n", className, d.since(), d.forRemoval());
        } catch (ClassNotFoundException e) {
            System.out.printf("%-34s ClassNotFoundException%n", className);
        }
    }

    // javac warns with [removal] when code uses an API marked for removal; suppress it here
    @SuppressWarnings("removal")
    public static void main(String[] args) {
        // JEP 398: the Applet API is now deprecated for removal
        printDeprecation("java.applet.Applet");

        // JEP 411: the Security Manager is deprecated for removal too
        printDeprecation("java.lang.SecurityManager");

        // JEP 407: RMI Activation is removed (the rest of java.rmi stays)
        printDeprecation("java.rmi.activation.Activatable");
        printDeprecation("java.rmi.Remote");

        // JEP 410: the experimental AOT compiler (jaotc) and the Graal JIT are removed
        Path bin = Path.of(System.getProperty("java.home"), "bin");
        System.out.printf("%-34s %s%n", "bin/jaotc exists", Files.exists(bin.resolve("jaotc")));
        System.out.printf("%-34s %s%n", "module jdk.internal.vm.compiler", ModuleLayer.boot().findModule("jdk.internal.vm.compiler").isPresent());

        // Trying to enable the Security Manager prints a warning (on standard error)
        System.out.println();
        System.out.flush();
        System.setSecurityManager(new SecurityManager());
        System.out.println("Security Manager installed: " + (System.getSecurityManager() != null));
    }
}
java.applet.Applet                 @Deprecated(since="9", forRemoval=true)
java.lang.SecurityManager          @Deprecated(since="17", forRemoval=true)
java.rmi.activation.Activatable    ClassNotFoundException
java.rmi.Remote                    not deprecated
bin/jaotc exists                   false
module jdk.internal.vm.compiler    false

WARNING: A terminally deprecated method in java.lang.System has been called
WARNING: System::setSecurityManager has been called by RemovalsDemo (file:/path/to/java17/RemovalsDemo.java)
WARNING: Please consider reporting this to the maintainers of RemovalsDemo
WARNING: System::setSecurityManager will be removed in a future release
Security Manager installed: true

Applet API deprecated for removal (JEP 398)

Applets no longer run in any browser. The java.applet package and related classes, deprecated since Java 9, are deprecated for removal (forRemoval=true) in Java 17.

Since then: removed in JDK 26 (JEP 504).

Security Manager deprecated for removal (JEP 411)

The Security Manager was a way to restrict untrusted code (such as applets) inside the JVM. It was hard to configure and almost nobody used it, yet it made much of the JDK pay for permission checks. Java 17 deprecates it for removal, and calling System.setSecurityManager() prints a warning like the one above.

flowchart TB
    A["JDK 17<br/>deprecated for removal, warning"] --> B["JDK 18<br/>disallowed by default<br/>(possible with allow)"]
    B --> C["JDK 24 (JEP 486)<br/>permanently disabled"]
    style C fill:#fecaca,stroke:#b91c1c,color:#7f1d1d

Since then: since JDK 18, setSecurityManager() throws UnsupportedOperationException unless you pass -Djava.security.manager=allow. It was permanently disabled in JDK 24 (JEP 486), and there is no longer any way to enable it.

RMI Activation removed (JEP 407)

The mechanism for starting RMI objects on demand (java.rmi.activation) is removed. It had been deprecated for removal in Java 15. RMI itself (java.rmi.Remote and so on) remains.

Experimental AOT and JIT compiler removed (JEP 410)

The AOT compiler added in Java 9 (jaotc) and Graal, the JIT compiler written in Java (an experimental feature), are removed. Almost nobody used them, and they were costly to maintain. If you want Graal, use GraalVM.

Since then: Project Leyden has taken over the goal of faster startup in a different way. JDK 24 added an AOT cache that stores the results of class loading and linking (JEP 483), JDK 25 extended it to method profiles (JEP 515), and JDK 26 to object caching that works with any GC (JEP 516).

Platforms

macOS/AArch64 port (JEP 391)

There is now an official JDK that runs natively on Apple Silicon (M1 and later) Macs. Before this, you had to run the x64 build through Rosetta 2. The test environment of this article uses this port.

java -XshowSettings:properties -version 2>&1 | grep -E 'os\.(arch|name)'
    os.arch = aarch64
    os.name = Mac OS X

New macOS rendering pipeline (JEP 382)

Since Apple deprecated OpenGL, Swing and Java 2D gained a rendering pipeline that uses Apple's Metal API. In Java 17, OpenGL is still the default; pass -Dsun.java2d.metal=true to use Metal.

Since then: Metal became the default in JDK 19. The OpenGL pipeline is still there, and -Dsun.java2d.opengl=true switches back to it.

APIs outside the JEPs: HexFormat and InstantSource

Java 17 also added a few handy APIs that did not get a JEP.

  • java.util.HexFormat: converts between bytes and hex strings. Previously you had to write this yourself or use a library.
  • java.time.InstantSource: an interface that only returns "now". It is a Clock without a time zone, and Clock itself implements it.
// Java 17 APIs outside the JEPs: HexFormat and InstantSource
// Run: java HexFormatDemo.java

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.InstantSource;
import java.util.HexFormat;

public class HexFormatDemo {

    // InstantSource: an interface that only provides "now" (a Clock without a time zone)
    record Token(String value, Instant expiresAt) {
        boolean isExpired(InstantSource time) {
            return time.instant().isAfter(expiresAt);
        }
    }

    public static void main(String[] args) {
        // HexFormat: convert between bytes and hex strings (previously hand-written or from a library)
        byte[] bytes = "Java17".getBytes(StandardCharsets.UTF_8);
        HexFormat hex = HexFormat.of();
        String encoded = hex.formatHex(bytes);
        System.out.println("formatHex         = " + encoded);
        System.out.println("parseHex          = " + new String(hex.parseHex(encoded), StandardCharsets.UTF_8));
        System.out.println("MAC style         = " + HexFormat.ofDelimiter(":").withUpperCase().formatHex(bytes));
        System.out.println("toHexDigits(255)  = " + hex.toHexDigits((byte) 255));

        // Tests can pass an InstantSource with a fixed time
        Instant t0 = Instant.parse("2026-08-01T00:00:00Z");
        Token token = new Token("abc", t0.plus(Duration.ofMinutes(10)));
        System.out.println();
        System.out.println("expired at +5 min  = " + token.isExpired(InstantSource.fixed(t0.plusSeconds(300))));
        System.out.println("expired at +15 min = " + token.isExpired(InstantSource.fixed(t0.plusSeconds(900))));
        System.out.println("Clock is an InstantSource = " + (Clock.systemUTC() instanceof InstantSource));
    }
}
formatHex         = 4a6176613137
parseHex          = Java17
MAC style         = 4A:61:76:61:31:37
toHexDigits(255)  = ff

expired at +5 min  = false
expired at +15 min = true
Clock is an InstantSource = true

If your code takes an InstantSource instead of calling Instant.now() directly, tests can fix the time.

Summary: checklist for moving to Java 17

Finally, here is a summary from the point of view of moving from Java 11 to 17.

CategoryFeatureVerdict
Use nowRecords, text blocks, switch expressions, instanceof patterns, sealed classes, HexFormatStandard features. Use them freely in new code
Check when migratingStrong encapsulation (403)If you see InaccessibleObjectException, update the library first
Check when migratingRemoval and deprecation of the Security Manager, RMI Activation and AOTSearch for uses. If you find any, plan a replacement
Consider replacingRandom API (356), deserialization filters (415)Review any Random shared across threads, and code that uses ObjectInputStream
WaitPatterns in switch (406), FFM API, Vector APIPreviews or incubators in Java 17. For production, use Java 21 or later

Java 17 is an LTS whose value lies less in individual JEPs than in six years of accumulated changes since Java 11. Records, sealed types and pattern matching let you describe data with types and handle each type safely. At the same time, changes such as strong encapsulation can get in the way of a migration, so update your dependencies to their latest versions first, then move.

References

© 2020 - 2026 issyuu @Lakeheart Retreat
Powered by theme astro-koharu · Inspired by Shoka
Loading...