JDK17 新特性
Java 17 LTS 是 Java SE 平台的最新长期支持版本。包含14个增强提案。
Java之父 James Gosling 接受InfoQ专访时说:新系统全方位性能更强、速度更快、错误也更少、扩展效率更高。是时候接纳JDK17了。
为了后续的版本更新,我们有必要了解下新特性。
Java17 14项JEP
JEP 306: Restore Always-Strict Floating-Point Semantics(恢复始终严格的浮点语义)
JEP 356: Enhanced Pseudo-Random Number Generators(增强的伪随机数生成器)
JEP 382: New macOS Rendering Pipeline(新的 macOS 渲染管道)
JEP 391: macOS/AArch64 Port(macOS/AArch64 端口)
JEP 398: Deprecate the Applet API for Removal(弃用 Applet API 以进行删除)
JEP 403: Strongly Encapsulate JDK Internals(强烈封装 JDK 内部)
JEP 406: Pattern Matching for switch (Preview)(开关的模式匹配)
JEP 407: Remove RMI Activation(删除 RMI 激活)
JEP 409: Sealed Classes(密封类)
JEP 410: Remove the Experimental AOT and JIT Compiler(删除实验性 AOT 和 JIT 编译器)
JEP 411: Deprecate the Security Manager for Removal(弃用安全管理器以进行删除)
JEP 412: Foreign Function & Memory API (Incubator) (外部函数和内存 API(孵化器))
JEP 414: Vector API (Second Incubator) (矢量 API(孵化器))
JEP 415: Implement Context-Specific Deserialization Filters(特定于上下文的反序列化过滤器)
JEP 356: Enhanced Pseudo-Random Number Generators
为伪随机数生成器 (PRNG) 提供新的接口类型和实现。
目标
-
更容易在应用程序中互换使用各种 PRNG 算法。
-
通过提供 PRNG 对象流更好地支持基于流的编程。
-
消除现有 PRNG 类中的代码重复。
-
小心保留 class 的现有行为java.util.Random。
描述
提供了一个新的接口,RandomGenerator它为所有现有的和新的 PRNG 提供了一个统一的 API。RandomGenerators提供名为ints、longs、doubles、nextBoolean、nextInt、nextLong、 nextDouble和的方法nextFloat及其所有当前参数变化。
可通过新类RandomGeneratorFactory来构造RandomGenerator的实例。
JEP 403: Strongly Encapsulate JDK Internals
强烈封装 JDK 的所有内部元素,除了 关键的内部 API,例如sun.misc.Unsafe. 将不再可能通过单个命令行选项放松对内部元素的强封装,这在 JDK 9 到 JDK 16 中是可能的。
目标
-
继续提高 JDK 的安全性和可维护性,这是Jigsaw 项目的主要目标之一。
-
鼓励开发人员从使用内部元素迁移到使用标准 API,这样他们和他们的用户都可以毫不费力地升级到未来的 Java 版本。
JEP 406: Pattern Matching for switch (Preview)
通过对 switch 表达式和语句的模式匹配以及对模式语言的扩展来增强 Java 编程语言。将模式匹配扩展到 switch 允许针对多个模式测试表达式,每个模式都有特定的操作,因此可以简洁安全地表达复杂的面向数据的查询。
在jdk16中:
// Old code
if (o instanceof String) {
String s = (String)o;
... use s ...
}
// New code
if (o instanceof String s) {
... use s ...
}
多路比较:
static String formatter(Object o) {
String formatted = "unknown";
if (o instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (o instanceof Long l) {
formatted = String.format("long %d", l);
} else if (o instanceof Double d) {
formatted = String.format("double %f", d);
} else if (o instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
扩展后:
static String formatterPatternSwitch(Object o) {
return switch (o) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> o.toString();
};
}
JEP 409: Sealed Classes
密封的类和接口限制了哪些其他类或接口可以扩展或实现它们。
密封类在JDK15中由 JEP 360提出,并在JDK 15中作为 预览功能提供。它们由JEP 397再次提出并进行了改进,并在 JDK 16中作为预览功能提供。此 JEP 建议在 JDK 17 中最终确定密封类,与 JDK 16 相比没有任何更改。
目标
-
允许类或接口的作者控制负责实现它的代码。
-
提供比访问修饰符更具声明性的方式来限制超类的使用。
-
通过为详尽的模式分析提供基础来支持模式匹配的未来方向。
描述
密封的类或接口只能由允许这样做的类和接口扩展或实现。
通过将sealed修饰符应用于其声明来密封类。然后,在任何extendsandimplements子句之后,该permits子句指定允许扩展密封类的类。例如,以下声明Shape指定了三个允许的子类:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square { ... }
指定的类permits必须位于超类附近:在同一个模块中(如果超类在命名模块中)或在同一个包中(如果超类在未命名模块中)。
指定的类permits必须具有规范名称,否则会报告编译时错误。这意味着匿名类和本地类不能成为密封类的子类型。
密封类对其允许的子类施加三个约束:
-
密封类及其允许的子类必须属于同一个模块,并且如果在未命名的模块中声明,则必须属于同一个包。
-
每个允许的子类都必须直接扩展密封类。
- 每个允许的子类都必须使用修饰符来描述它如何传播由其超类发起的密封:
-
可以声明允许的子类final以防止其在类层次结构中的一部分被进一步扩 展。(记录类是隐式声明的final。)
-
可以声明允许的子类sealed以允许其层次结构的一部分比其密封的超类所设想的扩展得更远,但以受限制的方式。
-
可以声明一个允许的子类non-sealed,以便它的层次结构部分恢复为对未知子类的扩展开放。密封类不能阻止其允许的子类这样做。(修饰符non-sealed是 为 Java 提出的第一个连字符关键字。)
-
JEP 412: Foreign Function & Memory API (Incubator)
引入一个 API,Java 程序可以通过该 API 与 Java 运行时之外的代码和数据进行互操作。通过有效地调用外部函数(即 JVM 之外的代码)和安全地访问外部内存(即不受 JVM 管理的内存),API 使 Java 程序能够调用本机库并处理本机数据,而不会出现脆弱性和危险。 JNI。
目标
-
易于使用— 将 Java 原生接口 ( JNI ) 替换为卓越的纯 Java 开发模型。
-
性能 — 提供与现有 API(如 JNI 和sun.misc.Unsafe.
-
通用性——提供在不同种类的外来内存(例如,本地内存、持久内存和托管堆内存)上操作的方法,并随着时间的推移适应其他平台(例如,32 位 x86)和用其他语言编写的外来函数比 C(例如,C++、Fortran)。
-
安全——默认禁用不安全操作,只有在应用程序开发人员或最终用户明确选择后才允许它们。
存储在 Java 运行时之外的内存中的数据称为堆外数据。(堆是 Java 对象所在的地方——堆上数据——以及垃圾收集器完成工作的地方。)访问堆外数据对于流行的 Java 库(如Tensorflow、Ignite、Lucene和Netty )的性能至关重要,主要是因为它可以让他们避免与垃圾收集相关的成本和不可预测性。它还允许通过将文件映射到内存来对数据结构进行序列化和反序列化,例如mmap. 然而,Java 平台目前还没有提供令人满意的访问堆外数据的解决方案。
-
ByteBufferAPI允许创建在堆外分配的直接字节缓冲区,但它们的最大大小为 2 GB,并且不会立即释放。这些和其他限制源于这样一个事实,即ByteBufferAPI 不仅设计用于堆外内存访问,而且还用于在字符集编码/解码和部分 I/O 操作等领域进行批量数据的生产者/消费者交换。在这种情况下,不可能满足多年来提交的许多堆外增强请求(例如,4496703、6558368、4837564和5029431)。
-
sun.misc.UnsafeAPI公开了对堆上数据的内存访问操作,这些操作也适用于堆外数据。UsingUnsafe是高效的,因为它的内存访问操作被定义为 HotSpot JVM 内在函数并由 JIT 编译器优化。但是,使用Unsafe是危险的,因为它允许访问任何内存位置。这意味着 Java 程序可以通过访问已经释放的位置来使 JVM 崩溃;由于这个和其他原因,Unsafe一直强烈反对使用.
使用 JNI 调用本机库,然后访问堆外数据是可能的,但性能开销很少使其适用:从 Java 到本机比访问内存慢几个数量级,因为 JNI 方法调用不能从许多常见的JIT 优化,例如内联。
总之,在访问堆外数据时,应该选择安全但低效的路径(ByteBuffer)还是应该放弃安全而追求性能(Unsafe)?他们需要的是一个受支持的 API,用于访问堆外数据(即外部内存),从一开始就设计为安全并考虑到 JIT 优化。
描述
Foreign Function & Memory API (FFM API) 定义了类和接口,以便库和应用程序中的客户端代码可以:
-
分配外来内存 (MemorySegment、、MemoryAddress和SegmentAllocator),
-
操作和访问结构化的外来记忆 (MemoryLayout、、MemoryHandles和MemoryAccess),
-
管理外部资源的生命周期 ( ResourceScope),以及
-
调用外部函数(SymbolLookup和CLinker)。 FFM API 位于模块的jdk.incubator.foreign包中。jdk.incubator.foreign。
官网代码例子:
作为使用 FFM API 的一个简短示例,下面是 Java 代码,它获取 C 库函数的方法句柄radixsort,然后使用它对 Java 数组中的四个字符串进行排序(省略了一些细节):
// 1. Find foreign function on the C library path
MethodHandle radixSort = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("radixsort"), ...);
// 2. Allocate on-heap memory to store four strings
String[] javaStrings = { "mouse", "cat", "dog", "car" };
// 3. Allocate off-heap memory to store four pointers
MemorySegment offHeap = MemorySegment.allocateNative(
MemoryLayout.ofSequence(javaStrings.length,
CLinker.C_POINTER), ...);
// 4. Copy the strings from on-heap to off-heap
for (int i = 0; i < javaStrings.length; i++) {
// Allocate a string off-heap, then store a pointer to it
MemorySegment cString = CLinker.toCString(javaStrings[i], newImplicitScope());
MemoryAccess.setAddressAtIndex(offHeap, i, cString.address());
}
// 5. Sort the off-heap data by calling the foreign function
radixSort.invoke(offHeap.address(), javaStrings.length, MemoryAddress.NULL, '\0');
// 6. Copy the (reordered) strings from off-heap to on-heap
for (int i = 0; i < javaStrings.length; i++) {
MemoryAddress cStringPtr = MemoryAccess.getAddressAtIndex(offHeap, i);
javaStrings[i] = CLinker.toJavaStringRestricted(cStringPtr);
}
assert Arrays.equals(javaStrings, new String[] {"car", "cat", "dog", "mouse"}); // true
这段代码比使用 JNI 的任何解决方案都清晰得多,因为隐藏在native方法调用后面的隐式转换和内存取消引用现在直接用 Java 表示。也可以使用现代 Java 习语;例如,流可以允许多个线程在堆上和堆外内存之间并行复制数据。
JEP 415: Implement Context-Specific Deserialization Filters
允许应用程序通过 JVM 范围的过滤器工厂配置特定于上下文和动态选择的反序列化过滤器,该过滤器工厂被调用来为每个单独的反序列化操作选择一个过滤器。
参考资料
-
Oracle官网:https://www.oracle.com/java/technologies/javase/17all-relnotes.html
-
InfoQ:《架构师》2022年2月