前言
Java 17 是 2021 年 9 月发布的 LTS(长期支持)版本。它是 Java 11 之后的下一个 LTS,又被 Spring Boot 3 定为最低要求,所以至今仍是很多项目的基准版本。另一方面,Oracle 对 Java 17 的 Premier Support 将在 2026 年 9 月结束,现在正好是盘点“还停在 17 的项目”的时候。
本文 一边实际运行,一边 介绍 Java 17 的 14 个 JEP。所有示例代码都是单个文件,不依赖任何外部库,复制下来就能直接用 java 命令运行。
Java 17 的 JEP 里,有不少是删除、弃用这类“不起眼”的内容。不过从 Java 11 升级时,Java 12〜16 中正式发布的 record、文本块等也会一起到手。所以在进入正题之前,先简单回顾一下这些特性。另外,每个特性的最后都附上 「后续进展」,补充到 JDK 26(2026 年 3 月 GA)为止的变化。
验证环境与示例的运行方法
- JDK:OpenJDK 17(17.0.20)
- 操作系统 / CPU:macOS(Apple Silicon,8 核)
- 直接运行源文件(JEP 330,Java 11 起支持),不需要先编译。
java SealedClassesDemo.java # 正式特性
java --enable-preview --source 17 SwitchPatternDemo.java # 预览特性- 示例代码也放在 GitHub 的 smile-lxy/java-new-features 上。代码注释用日语、中文、英语三种语言写成,本文只保留中文注释。
- 每个示例开头的注释(
// Run:)写有运行命令。 - 输出中的时间、哈希值(如
@6b1274d2),会随运行环境和每次运行而变化。
全貌:Java 17 的 14 个 JEP
| JEP | 特性 | 在 Java 17 中的状态 | 后续进展(〜JDK 26) |
|---|---|---|---|
| 306 | 恢复始终严格的浮点语义 | 正式 | ― |
| 356 | 增强的伪随机数生成器 | 正式 | JDK 19 新增 Random.from() |
| 382 | macOS 新渲染管线 | 新增(默认仍是 OpenGL) | JDK 19 起默认使用 Metal |
| 391 | macOS/AArch64 移植 | 正式 | ― |
| 398 | Applet API 标记为将要删除 | 将要删除 | JDK 26 删除 |
| 403 | 强封装 JDK 内部 API | 正式 | sun.misc.Unsafe 的内存访问也逐步受限 |
| 406 | Pattern Matching for switch | 预览 | JDK 21 正式发布 |
| 407 | 删除 RMI Activation | 删除 | ― |
| 409 | Sealed Classes | 正式 | ― |
| 410 | 删除实验性的 AOT / JIT 编译器 | 删除 | AOT 以 Project Leyden 的形式换了个方式回归 |
| 411 | Security Manager 标记为将要删除 | 将要删除 | JDK 24 永久禁用 |
| 412 | Foreign Function & Memory API | 孵化 | JDK 22 正式发布 |
| 414 | Vector API | 第 2 次孵化 | JDK 26 仍是第 11 次孵化 |
| 415 | 按上下文的反序列化过滤器 | 正式 | ― |
这些可以大致分为 4 类。
flowchart LR
J17["Java 17"] --> L["语言特性"]
J17 --> A["类库"]
J17 --> S["安全・维护"]
J17 --> P["平台"]
L --> L1["Sealed Classes(409)"]
L --> L2["switch 模式(406)"]
L --> L3["strictfp 的整理(306)"]
A --> A1["随机数 API(356)"]
A --> A2["FFM API(412)"]
A --> A3["Vector API(414)"]
S --> S1["强封装(403)"]
S --> S2["反序列化过滤器(415)"]
S --> S3["删除・弃用(398 / 407 / 410 / 411)"]
P --> P1["macOS/AArch64(391)"]
P --> P2["Metal 渲染(382)"]
classDef star fill:#fde68a,stroke:#b45309,color:#78350f
class L1,A1,S1 star
本文按照 回顾 Java 12〜16 → 语言特性 → 类库 → 安全・维护 → 平台 的顺序介绍。尤其是直接影响日常代码的 3 项(黄色),会详细展开。
前提:Java 12〜16 中正式发布的特性
从 Java 11 升级到 17,12〜16 之间正式发布的特性也会一起变得可用。Java 17 的 sealed 类和 switch 模式匹配都建立在它们之上,所以先把它们过一遍。
| 版本 | 主要特性 |
|---|---|
| Java 12 | Collectors.teeing、String.indent / transform |
| Java 14 | switch 表达式(JEP 361)、更易懂的 NullPointerException(JEP 358) |
| Java 15 | 文本块(JEP 378)、ZGC 和 Shenandoah 正式发布 |
| Java 16 | record(JEP 395)、instanceof 模式匹配(JEP 394)、Stream.toList()、jpackage |
flowchart TB
R["record(16)<br/>声明数据的形状"] --> S["sealed(17)<br/>封闭类型的种类"]
I["instanceof 模式(16)<br/>按类型判断并取出"] --> P["switch 模式(17 预览)<br/>按类型分支"]
W["switch 表达式(14)<br/>能返回值的 switch"] --> P
S --> P
P --> D["面向数据编程<br/>(Java 21 完成)"]
用 record 声明“数据的形状”,用 sealed 声明“类型的种类”,再用模式按类型分别处理。这条路线在 Java 21 的 record 模式中最终完成,但其中一大半在 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 {
// record(JEP 395,Java 16):自动生成构造器、访问器、equals、hashCode、toString
record Order(String id, int amount) {
// 用紧凑构造器校验参数
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) {
// 文本块(JEP 378,Java 15)
String json = """
{
"id": "A-1",
"amount": 300
}
""";
System.out.print("--- text block ---\n" + json);
// switch 表达式(JEP 361,Java 14):可以返回值,不需要 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);
}
// instanceof 模式匹配(JEP 394,Java 16):不需要强制转换
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)和 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);
// 更易懂的 NullPointerException(JEP 358,Java 14 加入,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请注意最后那条 NullPointerException 的消息。在 Java 11 之前这里只是 null,现在会告诉你 到底是哪个表达式为 null。光凭这一点就值得升级。
语言特性
Sealed Classes(JEP 409)
现在可以 限定哪些类型能继承 某个类或接口了。加上 sealed,并在 permits 中列出允许继承的类型。
flowchart TB
S["sealed interface Shape<br/>permits Circle, Square, Polygon"] --> C["record Circle<br/>(隐式 final)"]
S --> Q["final class Square"]
S --> P["non-sealed class Polygon"]
P --> T["class Triangle<br/>(任何类都可继承)"]
S -.->|"编译错误"| H["record Hexagon"]
style H fill:#fecaca,stroke:#b91c1c,color:#7f1d1d
被允许的子类,必须声明以下其中一种修饰符。
| 修饰符 | 含义 |
|---|---|
final | 不再允许继承(record 和 enum 隐式为 final) |
sealed | 继续用 permits 限定可以继承的类型 |
non-sealed | 解除限制,从这里开始任何类都可以继承 |
// JEP 409: Sealed Classes
// Run: java SealedClassesDemo.java
import java.util.Arrays;
import java.util.List;
public class SealedClassesDemo {
// 只有 permits 里列出的 3 个类型可以继承 Shape
sealed interface Shape permits Circle, Square, Polygon {}
// record 隐式为 final
record Circle(double radius) implements Shape {}
// final:不再允许继承
static final class Square implements Shape {
final double side;
Square(double side) { this.side = side; }
}
// non-sealed:从这里开始,任何类都可以继承
static non-sealed class Polygon implements Shape {
final int sides;
Polygon(int sides) { this.sides = sides; }
}
static class Triangle extends Polygon {
Triangle() { super(3); }
}
// 下面这一行会编译失败(Shape 拒绝 permits 以外的类型)
// record Hexagon() implements Shape {}
// Java 17 的 switch 还不能写模式(仍是预览),所以用 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) {
// 只凭边数算不出面积
return Double.NaN;
}
throw new AssertionError("unreachable: Shape is sealed");
}
public static void main(String[] args) {
// 可以用反射读取“密封”信息(JDK 17 新增的 API)
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把注释掉的 record Hexagon() implements Shape {} 恢复后,编译会像下面这样失败。
error: class is not allowed to extend sealed class: Shape (as it is not listed in its permits clause)
record Hexagon() implements Shape {}
^你可能会想:“不想被继承,加 final 不就行了?”但用 final 的话,连自己准备的子类也写不了。sealed 是用来 把“一共就这 3 种”这一领域知识表达成类型 的机制。而只要编译器知道种类是封闭的,下一节的 switch 就能检查“是否处理了所有情况”。
permits 中指定的类型必须放在同一个模块里(如果是未命名模块,则需在同一个包里)。另外,子类和父类在同一个文件中时,可以省略 permits。
Pattern Matching for switch(JEP 406・预览)
switch 的 case 里可以写 类型模式 了。这是把 Java 16 的 instanceof 模式匹配扩展到了 switch。在 Java 17 中它还是预览特性,需要 --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 {}
// 以前的写法:一连串 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(预览):case 里可以写类型模式,也可以写 case null
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";
};
}
// 没有 case null 的 switch 遇到 null 时,和以前一样抛出 NullPointerException
static String withoutNullCase(Object o) {
return switch (o) {
case String s -> "String";
default -> "other";
};
}
// 带守卫的模式:Java 17 用 && 加条件(JDK 19 改成了 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 是 sealed 的,所以不需要 default(编译器会检查是否穷尽)
};
}
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要点有 3 个。
- 类型模式:像
case Integer i这样,同时完成类型判断和变量赋值。比一连串 if-else 更易读,编译器也更容易优化。 case null:以前的 switch 只要传入null,一定会抛出 NullPointerException。写上case null,就能把null当作一种情况来处理。不写的话,还是和以前一样抛异常。- 穷尽性检查:
Shape是 sealed 的,所以写全Circle、Square、Rect三种就不需要default。如果给Shape加了第 4 种类型,这个 switch 会编译失败,漏改的地方一眼就能发现。
Java 17 的带守卫模式用 && 来写,例如 case Circle c && c.radius() > 10。这个语法在 JDK 19 被换成了 when,所以这个示例在 JDK 21 上无法编译。
SwitchPatternDemo.java:48: error: : or -> expected
case Circle c && c.radius() > 10 -> "large circle";
^switch 模式匹配在正式发布之前,经历了 4 次预览。
flowchart TB
A["JDK 17<br/>第 1 次预览<br/>(406)"] --> B["JDK 18<br/>第 2 次<br/>(420)"]
B --> C["JDK 19<br/>第 3 次(427)<br/>&& → when"]
C --> D["JDK 20<br/>第 4 次<br/>(433)"]
D --> E["JDK 21<br/>正式(441)<br/>+record 模式"]
style E fill:#bbf7d0,stroke:#15803d,color:#14532d
后续进展:JDK 21 正式发布(JEP 441)。record 模式(JEP 440)也同时正式发布,可以像 case Circle(double r) when r > 10 这样,一边解构 record 一边分支。从 JDK 23 开始,case int i 这类基本类型模式进入预览,JDK 26 中仍是第 4 次预览(JEP 530)。
恢复始终严格的浮点语义(JEP 306)
浮点运算现在不论有没有 strictfp,都 始终严格按照 IEEE 754 进行。
早年的 x87 FPU 在内部使用 80 位扩展精度计算。严格按 IEEE 754 的 64 位计算会变慢,所以 Java 1.2 让浮点运算有了两种语义:“默认稍微宽松,需要严格时才加 strictfp”。如今的 CPU(SSE2 以后的 x86 和 ARM)严格计算也不会变慢,于是 JEP 306 把它恢复成了 Java 1.2 之前的“始终严格”。
// JEP 306: Restore Always-Strict Floating-Point Semantics
// Run: java StrictFpDemo.java
public class StrictFpDemo {
// 在 Java 16 之前,有没有 strictfp 可能会影响中间结果的处理方式
static strictfp double withStrictfp(double x) {
return x * 10 / 10;
}
// 从 Java 17 开始,即使没有 strictfp,也总是严格按 IEEE 754 计算
static double withoutStrictfp(double x) {
return x * 10 / 10;
}
public static void main(String[] args) {
// x * 10 超过了 double 的最大值(约 1.8e308),所以中途就变成 Infinity
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 加了也没有意义,所以 javac 会给出警告。两边的计算结果相同,中途的 x * 10 溢出成了 Infinity。在过去的扩展精度环境中,没有 strictfp 的一边有可能算回 1.0E308。
对实际开发几乎没有影响。代码里如果有 strictfp,直接删掉即可。
增强的伪随机数生成器(JEP 356)
随机数生成器(PRNG)引入了 统一接口 RandomGenerator,可以根据用途选择算法了。
统一接口 RandomGenerator
以前的 Java 有 Random、ThreadLocalRandom、SplittableRandom,但它们没有共同的接口,很难替换算法。在 Java 17 中,它们都实现了 RandomGenerator,还按特性提供了一组子接口。
flowchart TB
RG["RandomGenerator<br/>nextInt / nextLong / nextDouble …"] --> ST["StreamableGenerator<br/>生成独立生成器的 Stream"]
ST --> SP["SplittableGenerator<br/>split()"]
ST --> JU["JumpableGenerator<br/>jump()"]
JU --> LE["LeapableGenerator<br/>leap()"]
LE --> AJ["ArbitrarilyJumpable<br/>Generator"]
SP --> LXM["LXM 系列(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) {
// 可用算法一览(分组、状态位数、特性)
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" : ""));
// 已有的类也实现了统一接口 RandomGenerator
System.out.println();
System.out.println("--- one interface ---");
// 第 1 个是以前的 Random,第 2 个按名字选择,第 3 个是默认算法
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));
}
// 种子相同,随机数序列就相同(可复现)
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():生成统计上相互独立的子生成器(适合并行处理)
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():把状态一下子推进 2^128 步(得到互不重叠的子序列)
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)选择算法的大致标准如下。
| 用途 | 选择 |
|---|---|
| 没有特别要求 | RandomGenerator.getDefault()(目前是 L32X64MixRandom) |
| 并行处理(Stream、Fork/Join) | 对 LXM 系列调用 split(),每个线程分一个 |
| 模拟中需要互不重叠的子序列 | Xoshiro 系列的 jump() / leap() |
| 加密、令牌 | 继续用 SecureRandom |
原有的 Random 照样可以用,也可以把 new Random() 传给参数类型为 RandomGenerator 的方法。
实测:多个线程共享 Random 的代价
java.util.Random 是线程安全的,但它用 AtomicLong 的 CAS 来更新内部状态(种子)。多个线程共享同一个 Random 时,就会争抢这个 CAS。下面用 8 个线程各调用 200 万次 nextLong(),比较耗时。
// 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;
// 用 THREADS 个线程,各调用 PER_THREAD 次 nextLong(),返回耗时(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(() -> {
// 在线程内部获取生成器(为了 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);
// 第 1 轮用于 JIT 预热,只显示之后的 3 轮
for (int round = 0; round <= 3; round++) {
// ① 所有线程共享 1 个 Random(争抢内部 AtomicLong 的 CAS)
long a = run(t -> shared);
// ② ThreadLocalRandom(每个线程有自己的状态)
long b = run(t -> ThreadLocalRandom.current());
// ③ 每个线程用 1 个 split() 出来的 L64X128MixRandom
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 个线程 × 各 200 万次 nextLong()(实测,3 轮平均,刻度单位: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
和每个线程各有状态的方式相比,共享的 Random 慢了几十倍。每次 CAS 失败都要重试,线程越多反而越慢。
ThreadLocalRandom 最快,是因为它把状态直接放在 Thread 对象里,而且 JIT 编译器对它做了特殊处理。如果并行处理需要 可复现(种子相同则结果相同),由于 ThreadLocalRandom 无法指定种子,应该用 split()。
后续进展:JDK 19 新增了 Random.from(RandomGenerator),可以把新算法传给要求 Random 的旧 API。
强封装 JDK 内部 API(JEP 403)
再也不能通过反射访问 JDK 的内部 API(sun.* 以及 java.* 的 private 成员)了。这是从 Java 9 引入模块系统以来逐步收紧限制的最后一步。
flowchart TB
A["JDK 9〜15<br/>--illegal-access=permit<br/>(只警告,仍允许)"] --> B["JDK 16(JEP 396)<br/>默认改为 deny"]
B --> C["JDK 17(JEP 403)<br/>废除 --illegal-access<br/>(指定了也会被忽略)"]
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 {
// ① 通过反射访问 java.lang.String 的私有字段 value
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());
}
// ② JDK 内部包(sun.nio.ch)里的类,同样无法通过反射打开
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());
}
// ③ sun.misc.Unsafe 在 jdk.unsupported 模块中,Java 17 仍可使用(属于例外)
Field theUnsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
System.out.println("(3) sun.misc.Unsafe: " + theUnsafe.get(null).getClass().getName());
// ④ java.base 是否向未命名模块 open 了 java.lang 包
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确实需要的话,可以用 --add-opens 按包显式地 打开。Java 16 之前的退路 --illegal-access=permit,在 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一些旧库(Lombok、Mockito、部分 ORM 和序列化库等)的旧版本可能依赖内部 API。迁移到 Java 17 时如果出现 InaccessibleObjectException,正确的做法是先升级这些库。--add-opens 最好只当作无法升级时的临时绕行办法。
sun.misc.Unsafe 等一部分内部 API,仍通过 jdk.unsupported 模块继续开放(③)。这是因为它们暂时还没有替代 API,作为过渡期保留了下来。
后续进展:随着替代品 VarHandle 和 FFM API 到位,sun.misc.Unsafe 也在逐步受限。内存访问相关的方法在 JDK 23 被标记为将要删除(JEP 471),从 JDK 24 起使用时会输出警告(JEP 498)。从 JDK 24 起使用 JNI 时也会输出警告(JEP 472),JDK 26 中通过反射修改 final 字段也会输出警告(JEP 500)。
按上下文的反序列化过滤器(JEP 415)
Java 的反序列化(ObjectInputStream)会把收到的字节流里写着的类,不管是什么都实例化出来。为了防御利用这一点的“反序列化攻击”,Java 9 引入了过滤器(JEP 290)。但整个 JVM 只能设置一个过滤器,很难做到“这个通信只允许这些类”这种 按场景的控制。
Java 17 可以注册 过滤器工厂 了。每当创建一个流,或者给流单独设置过滤器时,都会调用工厂,由它决定实际使用的过滤器。
sequenceDiagram
participant App as 应用
participant OIS as ObjectInputStream
participant F as 过滤器工厂
App->>OIS: new ObjectInputStream(...)
OIS->>F: apply(null, null)
F-->>OIS: 基本规则
App->>OIS: setObjectInputFilter(白名单)
OIS->>F: apply(基本规则, 白名单)
F-->>OIS: 合并后的过滤器
App->>OIS: readObject()
Note over OIS: 对每个类<br/>用合并后的过滤器判断
// 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 {}
// 可能被用于攻击的“意料之外的类”的示例
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 的基本规则:限制嵌套深度和数组长度
ObjectInputFilter base = ObjectInputFilter.Config.createFilter("maxdepth=10;maxarray=1000");
// Java 17 新功能:过滤器工厂。为每个流决定“使用哪个过滤器”
BinaryOperator<ObjectInputFilter> factory = (current, requested) -> {
if (requested == null) {
// 创建流时:应用基本规则
return base;
}
// 调用 setObjectInputFilter() 时:合并基本规则和个别规则,都无法判断时拒绝
return ObjectInputFilter.rejectUndecidedClass(ObjectInputFilter.merge(requested, current));
};
ObjectInputFilter.Config.setSerialFilterFactory(factory);
// 白名单(只允许 DeserializationFilterDemo$Order 和 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() + ")");
}
}
// 即使没有调用 setObjectInputFilter(),基本规则(maxarray=1000)也会生效
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 被拒绝了;即使没有设置白名单的流,大数组也会被基本规则(数组长度最多 1000)拒绝。关键在于:即使库自己不调用 setObjectInputFilter(),应用这边也能统一往安全的方向收紧。
这个特性很重要,所以也被反向移植到了 Java 8 和 11 的更新版本中。不过,根本的对策还是 不要用 Java 序列化接收不可信的数据。能用 JSON 之类格式的话,就选那些。
孵化 API
孵化(incubator)是比预览更早一步的“试验性 API”。它们放在 jdk.incubator.* 包中,必须用 --add-modules 显式添加才能使用。API 在下一个版本中可能会大幅改变。
Foreign Function & Memory API(JEP 412・孵化)
用来在不使用 JNI 的情况下 调用 C 库、安全地操作堆外内存 的 API。Java 17 的 JEP 412 把 Java 14 起的 Foreign Memory Access API(JEP 370 等)和 Foreign Linker API(JEP 389)合并成了一个。
// 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 {
// ① 不用 JNI,直接调用 C 标准库的 strlen
MethodHandle strlen = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("strlen").orElseThrow(),
MethodType.methodType(long.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER));
// 关闭 ResourceScope 时,在其中分配的本地内存会被一起释放
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);
// ② 在堆外分配 5 个 int 的空间并读写
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)");
// ③ 越界访问会抛出异常,而不是让 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如果用 JNI,就得写 C 代码、编译、准备共享库。用 FFM API,只写 Java 代码就能调用 strlen。分配的内存在关闭 ResourceScope 时释放,越界访问会抛出异常,而不会让 JVM 崩溃。
调用本地代码可能破坏 JVM 的安全性,因此需要用 --enable-native-access 显式允许。忘了加的话会像下面这样失败。
Exception in thread "main" java.lang.IllegalCallerException: Illegal native access from: unnamed module @4f638935后续进展:经过 JDK 18 的第 2 次孵化、JDK 19〜21 的预览,在 JDK 22 正式发布(JEP 454)。包名改成了 java.lang.foreign,API 也变化很大(ResourceScope → Arena、CLinker → Linker 等)。上面的代码只适用于 Java 17,在 JDK 22 及以后需要改写。另外,在正式版中,即使不加 --enable-native-access 也只会输出警告,程序会继续运行。
Vector API(JEP 414・第 2 次孵化)
用来在 Java 中显式编写利用 CPU SIMD 指令的向量运算的 API。一条指令同时计算多个元素(通道)。这里分别用普通循环和 Vector API 计算两个数组的点积,并做了比较。
// 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;
// 用普通循环计算两个数组的点积(a[0]*b[0] + a[1]*b[1] + ...)
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;
}
// 用 Vector API 做同样的计算,每次处理 SPECIES.length() 个元素
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(乘加运算)
acc = va.fma(vb, acc);
}
float sum = acc.reduceLanes(VectorOperators.ADD);
// 剩余的元素用普通(标量)方式处理
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));
// 对 100 万个元素的点积计算 500 次并比较耗时
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;
}
// 计时前先让两个方法都被 JIT 编译(预热)
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);
}
// 使用计算结果,防止计算被优化掉
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 ms100 万个元素的点积 × 500 次(实测,3 轮平均,刻度单位:s)
%%{init: {"gantt": {"leftPadding": 140}}}%%
gantt
dateFormat x
axisFormat %S.%L
tickInterval 200millisecond
section 普通循环
551 ms :crit, 0, 551
section Vector API
203 ms :active, 0, 203
在这个环境(Apple Silicon)中,每次处理 4 个 float,比普通循环快约 3 倍。
一开始没有预热就直接计时,结果 5 次中有 4 次 Vector API 反而更慢(普通循环约 550 ms,Vector API 为 700〜1100 ms)。只有当 JIT 编译器消除了 FloatVector 等对象、把它们换成向量指令之后,Vector API 才会变快。在这种优化生效之前计时,测到的只是一个“大量创建对象的循环”。测量 Vector API 的效果时,一定要加入预热,最好使用 JMH。
你可能会想:“JIT 编译器不是会自动向量化吗?”但浮点加法改变相加顺序时结果会有细微差别,所以 JIT 不能擅自把点积这种“累加”循环向量化。用 Vector API,就等于由程序员明确表示“顺序变了也没关系”。
后续进展:由于要等待 Valhalla 项目(值对象)完成,JDK 26 中仍是第 11 次孵化(JEP 529)。
删除・弃用
Java 17 也在继续清理旧功能。下面用一个示例一起确认。
// 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);
}
}
// 使用将要删除的 API 时 javac 会给出 [removal] 警告,这里将其屏蔽
@SuppressWarnings("removal")
public static void main(String[] args) {
// JEP 398:Applet API 被标记为“将要删除”
printDeprecation("java.applet.Applet");
// JEP 411:Security Manager 也被标记为“将要删除”
printDeprecation("java.lang.SecurityManager");
// JEP 407:RMI Activation 已被删除(java.rmi 的其他功能仍保留)
printDeprecation("java.rmi.activation.Activatable");
printDeprecation("java.rmi.Remote");
// JEP 410:实验性的 AOT 编译器(jaotc)和 Graal JIT 被删除
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());
// 尝试启用 Security Manager 时会输出警告(标准错误输出)
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 标记为将要删除(JEP 398)
Applet 早已无法在任何浏览器中运行。在 Java 9 被弃用的 java.applet 包等,在 Java 17 被标记为 将要删除(forRemoval=true)。
后续进展:在 JDK 26 被删除(JEP 504)。
Security Manager 标记为将要删除(JEP 411)
Security Manager 是用来在 JVM 中限制不可信代码(如 Applet)的机制。但它配置困难,实际上几乎没人用,却让 JDK 的大量代码背负着权限检查的负担。Java 17 把它标记为 将要删除,调用 System.setSecurityManager() 时会输出上面那样的警告。
flowchart TB
A["JDK 17<br/>将要删除・警告"] --> B["JDK 18<br/>默认禁止启用<br/>(指定 allow 才可以)"]
B --> C["JDK 24(JEP 486)<br/>永久禁用"]
style C fill:#fecaca,stroke:#b91c1c,color:#7f1d1d
后续进展:从 JDK 18 起,不加 -Djava.security.manager=allow 的话,setSecurityManager() 会抛出 UnsupportedOperationException。JDK 24 将其永久禁用(JEP 486),再也没有办法启用。
删除 RMI Activation(JEP 407)
按需启动 RMI 对象的机制(java.rmi.activation)被删除了。它在 Java 15 被标记为将要删除。RMI 本身(java.rmi.Remote 等)仍然保留。
删除实验性的 AOT / JIT 编译器(JEP 410)
Java 9 加入的 AOT 编译器(jaotc),以及用 Java 编写的 JIT 编译器 Graal(实验性功能)被删除了。原因是几乎没人使用,维护负担却很大。想用 Graal 的话,请使用 GraalVM。
后续进展:“加快启动”这个目标,由 Project Leyden 用另一种方式接了过去。JDK 24 引入了缓存类加载和链接结果的 AOT 缓存(JEP 483),JDK 25 扩展到方法的性能分析信息(JEP 515),JDK 26 又扩展到可用于任意 GC 的对象缓存(JEP 516)。
平台
macOS/AArch64 移植(JEP 391)
官方开始提供能在 Apple Silicon(M1 及以后)Mac 上原生运行的 JDK。在此之前,需要通过 Rosetta 2 运行 x64 版本。本文的验证环境用的就是这个移植版本。
java -XshowSettings:properties -version 2>&1 | grep -E 'os\.(arch|name)'
os.arch = aarch64
os.name = Mac OS XmacOS 新渲染管线(JEP 382)
由于 Apple 弃用了 OpenGL,Swing / Java 2D 新增了一条使用 Apple Metal API 绘制的渲染管线。Java 17 中默认仍是 OpenGL,加上 -Dsun.java2d.metal=true 才会使用 Metal。
后续进展:JDK 19 起默认使用 Metal。OpenGL 管线仍然保留,可以用 -Dsun.java2d.opengl=true 切换回去。
JEP 之外新增的 API:HexFormat 与 InstantSource
虽然没有对应的 JEP,Java 17 还新增了几个好用的 API。
java.util.HexFormat:字节数组与十六进制字符串的相互转换。以前需要自己写或者依赖外部库。java.time.InstantSource:只返回“当前时刻”的接口。相当于去掉了时区的Clock,Clock本身也实现了它。
// 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:只提供“当前时刻”的接口(不带时区的 Clock)
record Token(String value, Instant expiresAt) {
boolean isExpired(InstantSource time) {
return time.instant().isAfter(expiresAt);
}
}
public static void main(String[] args) {
// HexFormat:字节数组和十六进制字符串的相互转换(以前需要自己写或依赖外部库)
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));
// 在测试中可以传入固定时刻的 InstantSource
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不直接调用 Instant.now(),而是接收一个 InstantSource,测试时就可以固定时间。
总结:迁移到 Java 17 时的检查要点
最后,从 Java 11 迁移到 17 的角度做个整理。
| 分类 | 特性 | 判断 |
|---|---|---|
| 马上能用 | record、文本块、switch 表达式、instanceof 模式、sealed 类、HexFormat | 正式特性。从新写的代码开始积极使用 |
| 迁移时确认 | 强封装(403) | 出现 InaccessibleObjectException 时,先升级相关库 |
| 迁移时确认 | Security Manager、RMI Activation、AOT 的删除与弃用 | 搜索一下是否在用。如果在用,考虑替代方案 |
| 考虑替换 | 随机数 API(356)、反序列化过滤器(415) | 检查多线程共享的 Random,以及使用 ObjectInputStream 的地方 |
| 观望 | switch 模式(406)、FFM API、Vector API | 在 Java 17 中是预览或孵化。生产环境要用的话,请用 Java 21 及以后 |
Java 17 这个 LTS 的价值,与其说在于单个 JEP,不如说在于 从 Java 11 起 6 年间的累积。借助 record、sealed 和模式匹配,可以用类型来表达数据,并按类型安全地分别处理。另一方面,像强封装这样可能阻碍迁移的变化也存在,所以最好先把依赖库升级到最新,再进行迁移。
参考资料
- JDK 17 项目页面
- 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 系统属性(Oracle)