JDK 22 新特性详解
发布日期 :2024 年 3 月 19 日
规范版本 :Java SE 22(JSR 397)
是否 LTS :否
主题:外部函数与内存 API 终于转正、Unnamed Variables、Stream Gatherers 首次预览
一、版本总览
JDK 22 共集成 12 个 JEP,其中两个正式特性:
| JEP | 特性 | 状态 |
|---|---|---|
| 454 | Foreign Function & Memory API | 正式(历经 8 轮孵化/预览) |
| 456 | Unnamed Variables & Patterns | 正式 |
| 458 | Launch Multi-File Source-Code Programs | 正式 |
| 423 | Region Pinning for G1 | 正式 |
| 461 | Stream Gatherers | 预览 |
| 462 | Structured Concurrency | 第二次预览 |
| 463 | Implicitly Declared Classes and Instance Main Methods | 第二次预览 |
| 464 | Scoped Values | 第二次预览 |
| 457 | Class-File API | 预览 |
| 459 | String Templates | 第二次预览(后被搁置) |
| 460 | Vector API | 第七次孵化 |
二、正式特性
1. JEP 454:外部函数与内存 API(正式)
这是 JDK 22 最重要的特性。历经:
| 版本 | JEP | 阶段 |
|---|---|---|
| 16 | 389 + 393 | 两个独立孵化 API |
| 17 | 412 | 合并孵化 |
| 18 | 419 | 第二次孵化 |
| 19 | 424 | 预览 |
| 20 | 434 | 第二次预览 |
| 21 | 442 | 第三次预览 |
| 22 | 454 | 正式 |
替代什么:
- JNI(复杂、易错、需要 C 代码与编译步骤)
sun.misc.Unsafe(不安全、非标准)ByteBuffer(限制多:2GB 上限、无手动释放)
核心概念:
| 概念 | 说明 |
|---|---|
| Arena | 内存生命周期管理(此前叫 MemorySession / ResourceScope) |
| MemorySegment | 一段连续内存(可以是堆内或堆外) |
| MemoryLayout | 内存布局描述(用于结构化访问) |
| ValueLayout | 基础类型的布局(JAVA_INT、JAVA_LONG 等) |
| Linker | 本地函数链接器(downcall / upcall) |
| SymbolLookup | 符号查找(查找本地函数地址) |
| FunctionDescriptor | 函数签名描述 |
Arena 生命周期:
java
import java.lang.foreign.*;
// ① 受限 Arena:单线程访问,确定性释放
try (Arena arena = Arena.ofConfined()) {
MemorySegment seg = arena.allocate(100);
// 使用 seg
} // 自动释放
// ② 共享 Arena:多线程访问
try (Arena arena = Arena.ofShared()) {
MemorySegment seg = arena.allocate(100);
// 多线程可访问
}
// ③ 自动 Arena:由 GC 管理释放(时间不确定)
Arena arena = Arena.ofAuto();
MemorySegment seg = arena.allocate(100);
// ④ 全局 Arena:永不释放(用于长期存活的数据)
Arena global = Arena.global();
MemorySegment seg = global.allocate(100);
堆外内存分配与访问:
java
try (Arena arena = Arena.ofConfined()) {
// 分配 1KB 堆外内存
MemorySegment segment = arena.allocate(1024);
// 使用 ValueLayout 读写
segment.set(ValueLayout.JAVA_INT, 0, 42);
segment.set(ValueLayout.JAVA_LONG, 8, 123456789L);
segment.set(ValueLayout.JAVA_DOUBLE, 16, 3.14159);
int i = segment.get(ValueLayout.JAVA_INT, 0);
long l = segment.get(ValueLayout.JAVA_LONG, 8);
double d = segment.get(ValueLayout.JAVA_DOUBLE, 16);
// 分配 UTF-8 字符串
MemorySegment str = arena.allocateFrom("Hello, FFM!");
System.out.println(str.getString(0)); // "Hello, FFM!"
// 分配数组
MemorySegment intArray = arena.allocateFrom(ValueLayout.JAVA_INT, 1, 2, 3, 4, 5);
intArray.getAtIndex(ValueLayout.JAVA_INT, 2); // 3
}
结构化布局(Struct Layout):
java
// 定义 C 结构体:struct Point { int x; int y; }
StructLayout pointLayout = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
// 创建 VarHandle 用于字段访问
VarHandle xHandle = pointLayout.varHandle(
MemoryLayout.PathElement.groupElement("x"));
VarHandle yHandle = pointLayout.varHandle(
MemoryLayout.PathElement.groupElement("y"));
try (Arena arena = Arena.ofConfined()) {
MemorySegment point = arena.allocate(pointLayout);
xHandle.set(point, 0L, 10); // 注意:访问时需提供偏移量
yHandle.set(point, 0L, 20);
System.out.println(xHandle.get(point, 0L)); // 10
System.out.println(yHandle.get(point, 0L)); // 20
}
数组布局:
java
// 定义 int[10]
SequenceLayout intArrayLayout = MemoryLayout.sequenceLayout(10, ValueLayout.JAVA_INT);
VarHandle elemHandle = intArrayLayout.varHandle(
MemoryLayout.PathElement.sequenceElement());
try (Arena arena = Arena.ofConfined()) {
MemorySegment arr = arena.allocate(intArrayLayout);
for (int i = 0; i < 10; i++) {
elemHandle.set(arr, 0L, (long) i, i * i);
}
System.out.println(elemHandle.get(arr, 0L, 5L)); // 25
}
调用 C 函数(downcall):
java
import java.lang.invoke.MethodHandle;
import java.lang.foreign.*;
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
// ① 调用 strlen
MethodHandle strlen = linker.downcallHandle(
stdlib.find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);
try (Arena arena = Arena.ofConfined()) {
MemorySegment cString = arena.allocateFrom("Hello, World!");
long len = (long) strlen.invokeExact(cString);
System.out.println("length = " + len); // 13
}
// ② 调用 qsort
MethodHandle qsort = linker.downcallHandle(
stdlib.find("qsort").orElseThrow(),
FunctionDescriptor.ofVoid(
ValueLayout.ADDRESS, // base
ValueLayout.JAVA_LONG, // nmemb
ValueLayout.JAVA_LONG, // size
ValueLayout.ADDRESS // compar
)
);
// ③ 使用 GetSystemTime 之类的平台 API
把 Java 方法作为回调传给 C(upcall):
java
// 定义比较函数
static int compareInts(MemorySegment a, MemorySegment b) {
int x = a.get(ValueLayout.JAVA_INT, 0);
int y = b.get(ValueLayout.JAVA_INT, 0);
return Integer.compare(x, y);
}
// 生成函数指针(stub)
MethodHandle compareHandle = MethodHandles.lookup().findStatic(
Main.class, "compareInts",
MethodType.methodType(int.class, MemorySegment.class, MemorySegment.class));
MemorySegment compareStub = linker.upcallStub(
compareHandle,
FunctionDescriptor.of(ValueLayout.JAVA_INT,
ValueLayout.ADDRESS, ValueLayout.ADDRESS),
arena);
// 用作 qsort 的比较函数
qsort.invokeExact(intArray, 5L, 4L, compareStub);
访问本地内存的指针:
java
// 从 C 返回的指针创建 MemorySegment
MemorySegment ptr = (MemorySegment) malloc.invokeExact(1024L);
// 指针的 reinterpret 需要指定大小
MemorySegment bounded = ptr.reinterpret(1024);
与 JNI 的对比:
| 维度 | JNI | FFM API |
|---|---|---|
| 语言 | 需要 C 代码 | 纯 Java |
| 编译步骤 | 需要 gcc 等 | 不需要 |
| 性能 | 较高 | 较高(接近 JNI,JDK 22 起甚至有优化超越) |
| 安全性 | 低(易崩溃) | 较高(边界检查、生命周期管理) |
| 可维护性 | 差 | 好 |
| 跨平台 | 需分别为每个平台编译 | 一份 Java 代码 |
| 推荐度 | 仅遗留代码 | 新项目首选 |
迁移建议:
- 新项目直接使用 FFM API
- 已有 JNI 代码可逐步迁移,重点是简单包装函数
- 用
--enable-native-access=模块名控制本地访问权限(JDK 24 起会警告,JEP 472)
访问限制(JDK 22 中仍宽松,后续收紧):
bash
# JDK 22:本地访问不产生警告
java MyApp
# JDK 24 起(JEP 472):会打印警告
# WARNING: A restricted method in java.lang.foreign.Linker has been called
# WARNING: Restricted methods will be blocked in a future release
# 需显式授权:
java --enable-native-access=ALL-UNNAMED MyApp
2. JEP 456:Unnamed Variables & Patterns(正式)
从 JDK 21(JEP 443)预览转正。
java
// ① 未使用的 catch 参数
try {
Integer.parseInt(input);
} catch (NumberFormatException _) {
System.out.println("not a number");
}
// ② 未使用的 lambda 参数
map.forEach((_, value) -> System.out.println(value));
// ③ record pattern 中忽略组件
record Point(int x, int y) { }
if (obj instanceof Point(int x, _)) {
System.out.println("x = " + x);
}
// ④ switch 中忽略类型
switch (obj) {
case Point(int x, _) -> "x = " + x;
case String _ -> "some string";
case Integer _ -> "some int";
default -> "other";
}
// ⑤ 未使用的局部变量(有副作用但不需要结果)
int _ = computeWithSideEffect();
// ⑥ 未使用的 for 循环变量
for (Order _ : orders) {
count++;
}
// ⑦ try-with-resources
try (var _ = openConnection()) {
doWork();
}
// ⑧ 未使用的嵌套 record pattern 组件
if (obj instanceof Line(Point(_, int y1), Point(_, int y2))) {
System.out.println(y1 + ", " + y2);
}
规则:
_是关键字(JDK 21 起),不能作为标识符- 同一作用域内可有多个
_(因为都没有名字) _不能用于「读取」(它没有名字)- 匿名类不能声明
_字段
价值:显式表达「我故意不使用这个变量」,让代码意图更清晰,IDE 与静态分析工具不再报警告。
示例:更清晰的代码
java
// 之前:需要为不使用的变量起名
try {
...
} catch (Exception ex) { // ex 未被使用,IDE 警告
logger.warn("operation failed");
}
// JDK 22:意图明确
try {
...
} catch (Exception _) { // 明确表示不使用
logger.warn("operation failed");
}
3. JEP 458:启动多文件源代码程序
在 JDK 11(JEP 330)单文件源码启动的基础上,支持多文件。
bash
# 有多个源文件时,直接运行主文件
java Main.java
# 编译器会自动编译 Main.java 引用的其它同目录源文件
示例:
text
project/
├── Main.java
├── Greeter.java
└── util/
└── Helper.java
java
// Main.java
public class Main {
public static void main(String[] args) {
Greeter greeter = new Greeter();
System.out.println(greeter.greet("World"));
System.out.println(Helper.uppercase("hello"));
}
}
java
// Greeter.java
public class Greeter {
String greet(String name) {
return "Hello, " + name + "!";
}
}
java
// util/Helper.java
package util;
public class Helper {
public static String uppercase(String s) {
return s.toUpperCase();
}
}
bash
$ java Main.java
Hello, World!
HELLO
重要规则:
| 规则 | 说明 |
|---|---|
| 入口类 | 第一个源文件中的第一个顶层类 |
| 自动编译 | 同目录及子目录下被引用的源文件 |
| 搜索范围 | 源文件的包声明 决定搜索路径(util/Helper.java 对应 package util) |
不产生 .class |
编译在内存中进行 |
| 不支持模块 | 源文件不能是模块化程序 |
<h1>性能</h1> |
每次运行都重新编译,不适合反复运行的大项目 |
对脚本化的价值:
bash
#!/usr/bin/java --source 22
// 可直接执行的 Java 脚本(配合 shebang)
public class Script {
public static void main(String[] args) {
System.out.println("Running as a script: " + String.join(",", args));
}
}
与 jshell 的区别:
| 工具 | 用途 |
|---|---|
jshell |
交互式 REPL(逐行输入) |
java Xxx.java |
运行源文件(一次性执行) |
javac + java |
传统编译运行 |
4. JEP 423:Region Pinning for G1
背景 :JNI 临界区(critical region) 中的代码会直接访问对象地址(GetPrimitiveArrayCritical / GetStringCritical)。在此期间,GC 不能移动这些对象。
问题 :G1 此前在 JNI 临界区期间会禁用整个 GC,导致在这些区域长期持有数组(如大数据传输)时,GC 停顿时间不可控。
变更 :G1 现在只固定(pin)包含这些对象的区域(Region),而不是禁用整个 GC。
bash
# 无需额外参数,默认启用
java -XX:+UseG1GC MyApp
收益:
- JNI 临界区期间 GC 仍可运行(除了被固定的区域)
- 显著降低 JNI 密集应用的 GC 停顿
- 减少「GC 停顿尖峰」
5. JEP 457:Class-File API(预览)
目标 :提供标准的 Java API 来解析、生成、转换 class 文件,替代第三方库(ASM、Byte Buddy 的核心部分)。
为什么需要:
- JVM 每个版本都会新增 class 文件的特性(新的常量、属性、指令)
- JDK 自身(javac、JFR、反射、Lambda 元工厂)都内部使用 ASM,但 ASM 更新滞后于 JDK
- 第三方框架(Kotlin、Groovy、JaCoCo、Mockito、Spring)都需要维护自己的 class 文件解析器
提供的能力:
java
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
// ① 解析 class 文件
ClassFile cf = ClassFile.of();
ClassModel classModel = cf.parse(Path.of("MyClass.class"));
System.out.println("class: " + classModel.thisClass().asInternalName());
for (MethodModel method : classModel.methods()) {
System.out.println(" method: " + method.methodName().stringValue()
+ method.methodType().stringValue());
}
// ② 生成 class 文件
byte[] bytes = cf.build(ClassDesc.of("com.example.Hello"), classBuilder -> {
classBuilder.withFlags(ClassFile.ACC_PUBLIC);
classBuilder.withMethodBody("main",
MethodTypeDesc.of(ConstantDescs.CD_void,
ClassDesc.of("java.lang.String").arrayType()),
ClassFile.ACC_PUBLIC | ClassFile.ACC_STATIC,
codeBuilder -> {
codeBuilder.getstatic(ClassDesc.of("java.lang.System"),
"out", ClassDesc.of("java.io.PrintStream"));
codeBuilder.ldc("Hello, Class-File API!");
codeBuilder.invokevirtual(ClassDesc.of("java.io.PrintStream"),
"println", MethodTypeDesc.of(ConstantDescs.CD_void,
ClassDesc.of("java.lang.String")));
codeBuilder.return_();
});
});
// ③ 转换 class 文件
byte[] transformed = cf.transformClass(ClassModel.of(bytes), (classBuilder, element) -> {
if (element instanceof MethodModel method) {
// 修改方法
classBuilder.withMethodBody(method.methodName(), method.methodType(),
method.flags().flagsMask(), codeBuilder -> {
// ... 重新生成方法体
});
} else {
classBuilder.with(element);
}
});
核心 API:
| 接口/类 | 说明 |
|---|---|
ClassFile |
入口(parse、build、transformClass) |
ClassModel |
类模型(methods()、fields()、thisClass()) |
MethodModel |
方法模型 |
FieldModel |
字段模型 |
CodeModel |
方法体模型 |
CodeBuilder |
方法体构建器 |
ClassBuilder |
类构建器 |
ClassElement |
类元素的 sealed 接口(MethodModel、FieldModel 等均实现) |
演进路径:
| 版本 | JEP | 状态 |
|---|---|---|
| 22 | 457 | 预览 |
| 23 | 466 | 第二次预览 |
| 24 | 484 | 正式 |
6. JEP 461:Stream Gatherers(预览)
目标 :为 Stream API 提供可扩展的中间操作机制。
问题 :Stream 的中间操作是固定的 (filter、map、flatMap、distinct、sorted、limit、takeWhile 等)。部分常见需求没有对应的内置操作:
java
// 需求 1:按字符串长度去重(而非按内容)
var result = Stream.of("foo", "bar", "baz", "quux")
.distinctBy(String::length) // ❌ 不存在
.toList();
// 期望:[foo, quux]
// 需求 2:固定窗口分组
var windows = Stream.iterate(0, i -> i + 1)
.windowFixed(3) // ❌ 不存在
.limit(2)
.toList();
// 期望:[[0,1,2], [3,4,5]]
// 需求 3:累积扫描
List<Integer> prefixSums = Stream.of(1, 2, 3, 4)
.scan((a, b) -> a + b) // ❌ 不存在
.toList();
// 期望:[1, 3, 6, 10]
Stream::gather(Gatherer) ------对应 Stream::collect(Collector) 的地位:
java
// 使用内置 Gatherers
import java.util.stream.Gatherers;
// ① windowFixed:固定大小窗口
List<List<Integer>> windows = Stream.iterate(0, i -> i + 1)
.gather(Gatherers.windowFixed(3))
.limit(2)
.toList();
// [[0, 1, 2], [3, 4, 5]]
// ② windowSliding:滑动窗口
List<List<Integer>> slides = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.toList();
// [[1,2,3], [2,3,4], [3,4,5]]
// ③ fold:累积折叠为单个结果
Optional<String> joined = Stream.of("a", "b", "c")
.gather(Gatherers.fold(() -> "", (acc, s) -> acc + s))
.findFirst();
// "abc"
// ④ scan:累积扫描
List<Integer> prefixSums = Stream.of(1, 2, 3, 4)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.toList();
// [1, 3, 6, 10]
// ⑤ mapConcurrent:并发映射(虚拟线程)
List<String> results = urls.stream()
.gather(Gatherers.mapConcurrent(16, url -> fetch(url)))
.toList();
自定义 Gatherer:
java
// 实现「按长度去重」
static <T, K> Gatherer<T, ?, T> distinctBy(Function<? super T, K> keyExtractor) {
return Gatherer.ofSequential(
HashSet::new, // 初始状态
(set, element, downstream) -> {
K key = keyExtractor.apply(element);
if (set.add(key)) { // 第一次见到该 key
return downstream.push(element);
}
return !downstream.isRejecting(); // 继续处理
}
);
}
var result = Stream.of("foo", "bar", "baz", "quux")
.gather(distinctBy(String::length))
.toList();
// [foo, quux]
Gatherer 的四个组成部分:
| 组件 | 说明 |
|---|---|
| initializer | 创建私有状态对象 |
| integrator | 处理每个元素,可向下游推送 0 个、1 个或多个元素 |
| combiner | 合并并行执行的中间状态 |
| finisher | 输入结束后做最终处理 |
能力范围:
- 一对一 :如
map - 一对多 :如
flatMap - 多对一 :如
reduce - 多对多:如窗口分组
- 短路:可提前结束(把无限流转为有限流)
- 有状态:可跟踪已见元素
- 并行:可实现并行友好的 Gatherer
演进路径:
| 版本 | JEP | 状态 |
|---|---|---|
| 22 | 461 | 预览 |
| 23 | 473 | 第二次预览 |
| 24 | 485 | 正式 |
7. JEP 462:结构化并发(第二次预览)
相比 JDK 21(JEP 453)的主要改进是 API 微调与 StructuredTaskScope 的完善。
java
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
Response handle() throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join().throwIfFailed();
return new Response(user.get(), order.get());
}
}
8. JEP 463:隐式声明类与实例 main 方法(第二次预览)
(JDK 21 的 JEP 445 首次预览,此处为第二次。)
目标:让 Java 的「Hello World」更简单,降低入门门槛。
java
// JDK 21/22 预览:隐式声明类
void main() {
System.out.println("Hello, World!");
}
关键变化:
- 顶层方法(不在
class中)------隐式声明类 main方法可以是实例方法 (不需要static)main方法可以没有参数 (不需要String[])- 隐式类自动
import常用 API(java.util等)
三层渐进:
java
// ① 最简:隐式类 + 实例 main
void main() {
System.out.println("Hello");
}
// ② 显式类名,仍是实例 main
class HelloWorld {
void main() {
System.out.println("Hello");
}
}
// ③ 传统形式(仍然完全支持)
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello");
}
}
main 方法的选择规则:
- 优先选择带
String[]参数的 - 其次选择无参数的
- 可以是
static或实例方法 - 可以
public、protected或包私有(不能private)
隐式类的限制:
- 只能放在未命名包中
- 自动
implements无接口 - 自动
extends Object - 不能被其它类引用(无名)
- 编译产物的类名由文件名推导(
Main.java→Main)
自动导入的包(隐式类中):
java.langjava.iojava.mathjava.netjava.nio.filejava.utiljava.util.concurrentjava.util.functionjava.util.stream
(JDK 25 的 JEP 512 转正,改名为 Compact Source Files and Instance Main Methods。)
9. JEP 464:Scoped Values(第二次预览)
(JDK 20 孵化、JDK 21 预览,此处第二次预览。)
java
import java.lang.ScopedValue;
public class Server {
static final ScopedValue<Request> REQUEST = ScopedValue.newInstance();
void handle(Request request) {
ScopedValue.where(REQUEST, request).run(() -> {
// 此处及以下调用链中都可访问 REQUEST.get()
process();
});
}
void process() {
Request req = REQUEST.get();
// ...
}
}
(JDK 25 的 JEP 506 转正。)
10. JEP 459:String Templates(第二次预览)
(JDK 21 JEP 430 首次预览。)注意:该特性在 JDK 23 中被搁置,未继续推进。
java
String name = "Alice";
String s = STR."Hello, \{name}!";
11. JEP 460:Vector API(第七次孵化)
继续孵化,改进对 ARM 与 x86 的支持。
java
import jdk.incubator.vector.*;
static final VectorSpecies<Integer> SPECIES = IntVector.SPECIES_PREFERRED;
int[] addArrays(int[] a, int[] b) {
int[] result = new int[a.length];
int i = 0, bound = SPECIES.loopBound(a.length);
for (; i < bound; i += SPECIES.length()) {
IntVector va = IntVector.fromArray(SPECIES, a, i);
IntVector vb = IntVector.fromArray(SPECIES, b, i);
va.add(vb).intoArray(result, i);
}
for (; i < a.length; i++) result[i] = a[i] + b[i];
return result;
}
12. JEP 464 / 其它
(已在上述章节覆盖。)
三、JDK 22 特性速查表
| 类别 | JEP | 特性 | 状态 |
|---|---|---|---|
| 库 | 454 | 外部函数与内存 API | 正式 |
| 语言 | 456 | Unnamed Variables & Patterns | 正式 |
| 工具 | 458 | 启动多文件源码程序 | 正式 |
| GC | 423 | G1 的 Region Pinning | 正式 |
| 库 | 461 | Stream Gatherers | 预览 |
| 库 | 457 | Class-File API | 预览 |
| 库 | 459 | String Templates | 第二次预览(搁置) |
| 库 | 460 | Vector API | 第七次孵化 |
| 并发 | 462 | Structured Concurrency | 第二次预览 |
| 并发 | 464 | Scoped Values | 第二次预览 |
| 语言 | 463 | 隐式声明类与实例 main | 第二次预览 |
四、升级注意事项
- FFM API 转正(JEP 454) :可放心在生产使用;但注意 JDK 24 起会要求
--enable-native-access _成为关键字(JEP 456) :检查代码中是否有名为_的标识符- 多文件源码启动:适合脚本与原型,不适合大型项目(每次重新编译)
- String Templates 已被搁置:不要在生产代码中使用
- Stream Gatherers 是预览:JDK 24 转正
- Class-File API 是预览:JDK 24 转正
- 结构化并发 / Scoped Values 仍在预览:API 还会变化
五、参考资源
- 官方发布页:https://openjdk.org/projects/jdk/22/
- JEP 454(FFM API):https://openjdk.org/jeps/454
- JEP 456(Unnamed Variables):https://openjdk.org/jeps/456
- JEP 461(Stream Gatherers):https://openjdk.org/jeps/461
- JEP 423(Region Pinning):https://openjdk.org/jeps/423
- JEP 457(Class-File API):https://openjdk.org/jeps/457