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
| JEP | Feature | Status in Java 17 | Since then (up to JDK 26) |
|---|---|---|---|
| 306 | Always-strict floating-point semantics | Standard | — |
| 356 | Enhanced pseudo-random number generators | Standard | JDK 19 added Random.from() |
| 382 | New macOS rendering pipeline | Added (OpenGL still the default) | Metal became the default in JDK 19 |
| 391 | macOS/AArch64 port | Standard | — |
| 398 | Applet API deprecated for removal | Deprecated for removal | Removed in JDK 26 |
| 403 | Strong encapsulation of JDK internals | Standard | Memory access through sun.misc.Unsafe is being restricted too |
| 406 | Pattern Matching for switch | Preview | Standard in JDK 21 |
| 407 | RMI Activation removed | Removed | — |
| 409 | Sealed Classes | Standard | — |
| 410 | Experimental AOT and JIT compiler removed | Removed | AOT came back in another form through Project Leyden |
| 411 | Security Manager deprecated for removal | Deprecated for removal | Permanently disabled in JDK 24 |
| 412 | Foreign Function & Memory API | Incubator | Standard in JDK 22 |
| 414 | Vector API | Second incubator | Still an incubator in JDK 26 (eleventh) |
| 415 | Context-specific deserialization filters | Standard | — |
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.
| Version | Main features |
|---|---|
| Java 12 | Collectors.teeing, String.indent / transform |
| Java 14 | Switch expressions (JEP 361), helpful NullPointerExceptions (JEP 358) |
| Java 15 | Text blocks (JEP 378), ZGC and Shenandoah become production-ready |
| Java 16 | Records (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 nullLook 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.
| Modifier | Meaning |
|---|---|
final | No further subclasses (records and enums are implicitly final) |
sealed | Restrict its own subclasses again with permits |
non-sealed | Lift 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 = NaNIf 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.0There are three points.
- Type patterns:
case Integer itests 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. case null: a switch used to throw NullPointerException whenever it gotnull. Withcase null,nullbecomes just another case. Without it, the switch still throws, as before.- Exhaustiveness checking:
Shapeis sealed, so coveringCircle,SquareandRectis enough; nodefaultis needed. If you add a fourth type toShape, 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.30000000000000004strictfp 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:
| Use | Choice |
|---|---|
| No special needs | RandomGenerator.getDefault() (currently L32X64MixRandom) |
| Parallel work (streams, Fork/Join) | split() an LXM generator and give one to each thread |
| Non-overlapping subsequences for simulations | jump() / leap() of the Xoshiro family |
| Cryptography, tokens | Keep 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 ms8 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: falseIf 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: falseOld 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 freedWith 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 @4f638935Since 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 msDot 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: trueApplet 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 XNew 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 aClockwithout a time zone, andClockitself 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 = trueIf 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.
| Category | Feature | Verdict |
|---|---|---|
| Use now | Records, text blocks, switch expressions, instanceof patterns, sealed classes, HexFormat | Standard features. Use them freely in new code |
| Check when migrating | Strong encapsulation (403) | If you see InaccessibleObjectException, update the library first |
| Check when migrating | Removal and deprecation of the Security Manager, RMI Activation and AOT | Search for uses. If you find any, plan a replacement |
| Consider replacing | Random API (356), deserialization filters (415) | Review any Random shared across threads, and code that uses ObjectInputStream |
| Wait | Patterns in switch (406), FFM API, Vector API | Previews 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
- JDK 17 project page
- JEP 409: Sealed Classes
- JEP 406: Pattern Matching for switch (Preview) / JEP 441: Pattern Matching for switch
- JEP 356: Enhanced Pseudo-Random Number Generators
- JEP 403: Strongly Encapsulate JDK Internals
- JEP 415: Context-Specific Deserialization Filters
- JEP 412: Foreign Function & Memory API (Incubator) / JEP 454: Foreign Function & Memory API
- JEP 414: Vector API (Second Incubator)
- JEP 411: Deprecate the Security Manager for Removal / JEP 486: Permanently Disable the Security Manager
- JEP 504: Remove the Applet API
- JEP 382: New macOS Rendering Pipeline
- Java 2D system properties (Oracle)