Java 17 新特性解析与代码示例
文章目录
引言
Java 17 是 Java SE 平台的最新长期支持(LTS)版本,于 2021 年 9 月 15 日发布。作为继 Java 11 之后的又一个 LTS 版本,Java 17 引入了多项新功能和改进,旨在提升开发者生产力、增强程序性能并提高安全性。根据 Oracle 的支持路线图,Java 17 将获得至少八年的支持,使其成为企业级应用的理想选择。
本文将深入探讨 Java 17 的主要新特性,包括密封类、switch 模式匹配(预览)、外部函数和内存 API(孵化)、增强的伪随机数生成器以及上下文特定的反序列化过滤器。每项特性都将配有详细的代码示例,对于优化现有功能的特性,我们将提供对比代码以展示改进。此外,我们还将简要介绍其他值得注意的变化,如 JDK 内部的强封装和安全管理器的弃用。
通过阅读本文,开发者将能够全面了解 Java 17 的新特性,并通过实际代码示例掌握如何在项目中应用这些特性。所有信息均基于 Oracle 官方文档和其他权威资源,确保准确性和可靠性。
1. 密封类(JEP 409)
1.1. 介绍
密封类是 Java 17 中引入的一项重要特性,允许开发者限制哪些类或接口可以扩展或实现某个类或接口。这一特性在 JDK 15 和 16 中作为预览功能引入,并在 Java 17 中正式标准化。密封类增强了继承控制,有助于设计更安全、更可维护的代码。
1.2. 详细说明
在传统 Java 中,类可以被任意类扩展,除非标记为 final
。然而,在某些场景下,开发者希望只允许特定的子类扩展基类。例如,在图形应用中,可能希望 Shape
类只能被 Circle
、Rectangle
和 Triangle
扩展。密封类通过 sealed
关键字和 permits
子句实现这一目标。
密封类的子类必须标记为 final
(不可再扩展)、sealed
(可以指定自己的子类)或 non-sealed
(允许任意扩展)。这一机制不仅提高了代码的安全性,还在模式匹配(如 switch 表达式)中提供了编译时检查,确保所有可能的情况都被处理。
1.3. 代码示例
以下是一个使用密封类定义图形层次结构的示例:
public sealed class Shape permits Circle, Rectangle, Triangle {
public abstract double area();
}
public final class Circle extends Shape {
private final double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public final class Rectangle extends Shape {
private final double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public final class Triangle extends Shape {
private final double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
}
在这个例子中,Shape
是一个密封类,只允许 Circle
、Rectangle
和 Triangle
扩展它。每个子类都被标记为 final
,表示它们不能被进一步扩展。
由于 Shape
是密封类,我们可以在 switch 表达式中利用其封闭性,确保覆盖所有可能的情况,而无需默认分支:
public class ShapeDemo {
public static String describe(Shape shape) {
return switch (shape) {
case Circle c -> "圆形,面积:" + c.area();
case Rectangle r -> "矩形,面积:" + r.area();
case Triangle t -> "三角形,面积:" + t.area();
};
}
public static void main(String[] args) {
Shape circle = new Circle(5.0);
Shape rectangle = new Rectangle(4.0, 6.0);
Shape triangle = new Triangle(3.0, 8.0);
System.out.println(describe(circle));
System.out.println(describe(rectangle));
System.out.println(describe(triangle));
}
}
输出:
圆形,面积:78.53981633974483
矩形,面积:24.0
三角形,面积:12.0
如果遗漏了某个子类,编译器会报错,确保代码的完整性。
1.4. 与之前功能的对比
在 Java 17 之前,控制继承通常需要使用 final
关键字或包私有类,但这些方法缺乏灵活性。例如,final
完全禁止扩展,而包私有类限制了访问范围。密封类提供了一种中间方案,允许开发者明确指定允许的子类,同时保持代码的开放性。
传统方式:
public abstract class Shape {
// 无法限制哪些类可以扩展
}
public class Circle extends Shape {
// ...
}
public class Rectangle extends Shape {
// ...
}
// 任何类都可以扩展 Shape,可能导致意外的子类
public class UnknownShape extends Shape {
// ...
}
密封类通过 permits
子句解决了这个问题,确保只有指定的子类可以扩展基类。
1.5. 使用场景
- API 设计:在设计库或框架时,限制用户扩展特定的类。
- 领域建模:在需要固定类型集合的场景中,如代数数据类型。
- 模式匹配:与 switch 表达式结合,确保所有情况都被处理。
1.6. 总结
密封类为 Java 开发者提供了更精细的继承控制,增强了代码的安全性和可维护性。通过与模式匹配结合,密封类还可以提高代码的健壮性,是 Java 17 中一项强大的新特性。
2. switch 模式匹配(预览,JEP 406)
2.1. 介绍
switch 模式匹配是 Java 17 中的一项预览特性,增强了 switch 语句和表达式的表达能力。它允许在 case 标签中使用模式匹配,从而简化类型检查和转换的代码。这一特性在 JDK 14 和 16 中逐步引入,并在 Java 17 中进一步完善。
2.2. 详细说明
传统 switch 语句需要显式的类型检查和转换,通常导致冗长的代码。switch 模式匹配允许直接在 case 标签中指定类型模式,自动完成类型检查和转换。此外,还支持守卫模式(guarded patterns),允许在模式中添加条件。
由于这是预览特性,使用时需要启用 --enable-preview
标志:
javac --enable-preview --release 17 MyClass.java
java --enable-preview MyClass
2.3. 代码示例
以下是一个使用 switch 模式匹配的示例:
public class PatternMatchingDemo {
public static void process(Object obj) {
switch (obj) {
case String s when s.length() > 5 -> System.out.println("长字符串:" + s);
case String s -> System.out.println("短字符串:" + s);
case Integer i -> System.out.println("整数:" + i * 2);
default -> System.out.println("未知类型");
}
}
public static void main(String[] args) {
process("Hello");
process("Hello, World!");
process(42);
process(new Object());
}
}
输出:
短字符串:Hello
长字符串:Hello, World!
整数:84
未知类型
在这个例子中,switch 表达式根据对象的类型和条件(字符串长度)执行不同的操作。
2.4. 与之前功能的对比
在 Java 17 之前,处理类似逻辑需要使用 instanceof
和显式转换:
public class OldPatternMatchingDemo {
public static void process(Object obj) {
if (obj instanceof String s) {
if (s.length() > 5) {
System.out.println("长字符串:" + s);
} else {
System.out.println("短字符串:" + s);
}
} else if (obj instanceof Integer i) {
System.out.println("整数:" + i * 2);
} else {
System.out.println("未知类型");
}
}
}
相比之下,switch 模式匹配更简洁,减少了样板代码,提高了可读性。
2.5. 使用场景
- 简化条件逻辑:在需要根据类型执行不同操作的场景中。
- 数据处理管道:在处理复杂数据结构时,减少嵌套的 if-else 语句。
- 与密封类结合:确保所有类型都被处理,提高代码健壮性。
2.6. 总结
switch 模式匹配显著提高了 switch 语句的表达能力,使代码更简洁、更易读。作为预览特性,开发者需要注意其 API 可能在未来版本中发生变化,但它已经展示了在简化复杂逻辑方面的巨大潜力。
3. 外部函数和内存 API(孵化,JEP 412)
3.1. 介绍
外部函数和内存 API 是 Java 17 中的一项孵化特性,旨在提供一种更安全、更高效的方式与本地代码和内存交互。它替代了传统的 Java 本地接口(JNI),减少了样板代码和潜在风险。
3.2. 详细说明
传统 JNI 要求编写复杂的 C 代码来桥接 Java 和本地库,容易出错且难以维护。外部函数和内存 API 提供了一种纯 Java 的方式来调用本地函数和访问堆外内存。它通过 jdk.incubator.foreign
包实现,使用时需要添加模块:
--add-modules jdk.incubator.foreign
3.3. 代码示例
以下是一个调用 C 标准库函数 strlen
的示例:
import jdk.incubator.foreign.CLinker;
import jdk.incubator.foreign.FunctionDescriptor;
import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.SymbolLookup;
import java.lang.invoke.MethodHandle;
public class ForeignFunctionDemo {
public static void main(String[] args) {
SymbolLookup stdlib = CLinker.systemLookup();
MemoryAddress strlenAddr = stdlib.lookup("strlen").orElseThrow();
FunctionDescriptor fd = FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER);
MethodHandle strlen = CLinker.getInstance().downcallHandle(strlenAddr, fd);
try (MemorySegment str = CLinker.toCString("Hello, World!")) {
long length = (long) strlen.invokeExact(str.address());
System.out.println("字符串长度:" + length);
} catch (Throwable e) {
e.printStackTrace();
}
}
}
输出:
字符串长度:13
这个示例展示了如何使用 API 调用 C 的 strlen
函数计算字符串长度。
3.4. 与之前功能的对比
在 Java 17 之前,调用 strlen
需要编写 JNI 代码,包括 C 文件和 Java 本地方法声明,复杂且容易出错。外部函数和内存 API 消除了这些复杂性,提供了更简洁的接口。
3.5. 使用场景
- 性能关键操作:与本地库交互以实现高性能计算。
- 硬件访问:直接操作硬件资源或堆外内存。
- 遗留系统集成:与现有的 C/C++ 库集成。
3.6. 总结
外部函数和内存 API 为 Java 与本地代码的交互提供了现代化的解决方案。尽管目前是孵化状态,但它展示了未来替代 JNI 的潜力。开发者在使用时需注意其实验性质。
4. 增强的伪随机数生成器(JEP 356)
4.1. 介绍
Java 17 引入了新的伪随机数生成器(PRNG)接口和实现,提供了更灵活、更高效的随机数生成方式。这一特性通过 java.util.random.RandomGenerator
接口实现,支持多种算法。
4.2. 详细说明
传统 java.util.Random
类功能有限,且在并行场景下性能不佳。Java 17 的新 PRNG API 引入了 RandomGenerator
接口,支持多种算法(如 LXM、SplittableRandom),并提供了跳跃(jumpable)和分裂(splittable)功能,适合并行处理。
4.3. 代码示例
以下是使用新 PRNG 的示例:
import java.util.random.RandomGenerator;
public class RandomDemo {
public static void main(String[] args) {
RandomGenerator rng = RandomGenerator.of("L32X64MixRandom");
System.out.println("随机整数:" + rng.nextInt());
System.out.println("随机双精度数:" + rng.nextDouble());
}
}
输出示例:
随机整数:123456789
随机双精度数:0.678912345
4.4. 与之前功能的对比
传统方式使用 java.util.Random
:
import java.util.Random;
public class OldRandomDemo {
public static void main(String[] args) {
Random random = new Random();
System.out.println("随机整数:" + random.nextInt());
System.out.println("随机双精度数:" + random.nextDouble());
}
}
新 API 提供了更多算法选择,并支持并行场景。例如,SplittableRandom
适合多线程应用:
import java.util.random.RandomGenerator;
import java.util.random.RandomGenerator.SplittableGenerator;
public class SplittableRandomDemo {
public static void main(String[] args) {
SplittableGenerator rng = RandomGenerator.of("SplittableRandom");
SplittableGenerator subRng = rng.split();
System.out.println("主生成器随机数:" + rng.nextInt());
System.out.println("子生成器随机数:" + subRng.nextInt());
}
}
4.5. 使用场景
- 模拟和游戏:需要高质量随机数的场景。
- 并行处理:在多线程环境中生成独立的随机数流。
- 密码学:选择适合安全需求的算法。
4.6. 总结
增强的伪随机数生成器提供了更灵活、更高效的随机数生成方式,特别适合并行和性能敏感的应用。开发者可以根据需求选择合适的算法。
5. 上下文特定的反序列化过滤器(JEP 415)
5.1. 介绍
反序列化是 Java 中一个已知的安全风险,可能导致任意代码执行。Java 17 引入了上下文特定的反序列化过滤器,允许开发者为每次反序列化配置过滤器,以提高安全性。
5.2. 详细说明
新特性通过 JVM 全局过滤器工厂实现,允许动态检查和拒绝不安全的序列化对象。开发者可以定义过滤器,只允许特定类被反序列化,从而降低攻击风险。
5.3. 代码示例
以下是定义和使用反序列化过滤器的示例:
import java.io.ObjectInputFilter;
public class DeserializationFilterDemo {
public static ObjectInputFilter createFilter() {
return (info) -> {
if (info.serialClass() != null && info.serialClass().getName().startsWith("com.example.")) {
return ObjectInputFilter.Status.ALLOWED;
}
return ObjectInputFilter.Status.REJECTED;
};
}
public static void main(String[] args) {
ObjectInputFilter.Config.setSerialFilter(createFilter());
// 反序列化代码
}
}
这个过滤器只允许 com.example
包中的类被反序列化。
5.4. 与之前功能的对比
在 Java 17 之前,反序列化安全依赖于全局过滤器或手动检查,配置复杂且不够灵活。新特性提供了更细粒度的控制,简化了安全配置。
5.5. 使用场景
- 安全敏感应用:防止反序列化攻击。
- 遗留系统:为现有代码添加安全层。
- 微服务:在分布式系统中保护数据传输。
5.6. 总结
上下文特定的反序列化过滤器显著提高了 Java 应用的安全性,是处理不信任数据时的重要工具。
6. JDK 内部的强封装(JEP 403)
6.1. 介绍
Java 17 通过 JEP 403 加强了 JDK 内部 API 的封装,默认禁止访问除关键 API(如 sun.misc.Unsafe
)外的内部元素,以提高安全性和可维护性。
6.2. 详细说明
在早期 Java 版本中,开发者可以通过反射访问 JDK 内部 API,这可能导致不稳定的代码。Java 17 默认启用强封装,禁止访问大多数内部 API,鼓励使用标准 API。
6.3. 影响与应对
开发者需要检查代码是否依赖内部 API,并寻找替代方案。例如,sun.misc.Unsafe
的替代方案可能包括新的标准 API 或第三方库。
6.4. 使用场景
- 迁移到 Java 17:检查和更新依赖内部 API 的代码。
- 安全增强:确保应用不依赖不稳定的内部实现。
6.5. 总结
强封装提高了 JDK 的安全性和稳定性,开发者应尽早迁移到标准 API。
7. 安全管理器的弃用(JEP 411)
7.1. 介绍
Java 17 弃用了安全管理器(Security Manager),计划在未来版本中移除。这一变化反映了 Java 安全模型的演变。
7.2. 详细说明
安全管理器用于限制代码的权限,但其复杂性和维护成本高。现代 Java 应用更倾向于使用模块化和容器化来实现安全隔离。
7.3. 影响与应对
开发者应评估是否依赖安全管理器,并考虑使用其他安全机制,如模块系统或操作系统级隔离。
7.4. 使用场景
- 遗留系统迁移:更新依赖安全管理器的代码。
- 现代安全实践:采用容器化或模块化方案。
7.5. 总结
安全管理器的弃用标志着 Java 安全模型的现代化,开发者需要适应新的安全实践。
8. 其他值得注意的特性
8.1. 新 macOS 渲染管道(JEP 382)
Java 17 引入了基于 Apple Metal 的新渲染管道,优化了 macOS 上的图形性能。默认禁用,可通过 -Dsun.java2d.metal=true
启用。
8.2. macOS/AArch64 支持(JEP 391)
Java 17 支持 macOS 上的 AArch64 架构(如 Apple M1 芯片),确保在新型 Mac 设备上的兼容性。
8.3. 十六进制格式化工具
java.util.HexFormat
类提供了便捷的十六进制转换功能,适用于字节数组和基本类型的格式化。
import java.util.HexFormat;
public class HexFormatDemo {
public static void main(String[] args) {
HexFormat hex = HexFormat.of().withUpperCase();
byte[] bytes = {0x1A, 0x2B, 0x3C};
String hexStr = hex.formatHex(bytes);
System.out.println("十六进制:" + hexStr);
}
}
输出:
十六进制:1A2B3C
8.4. 总结
这些特性进一步丰富了 Java 17 的功能,提供了更好的平台支持和实用工具。
9. 结论
Java 17 作为 LTS 版本,带来了多项重要的新特性和改进。密封类增强了继承控制,switch 模式匹配简化了条件逻辑,外部函数和内存 API 提供了与本地代码交互的新方式,增强的伪随机数生成器提高了随机数生成的灵活性,而上下文特定的反序列化过滤器加强了安全性。此外,JDK 内部的强封装和安全管理器的弃用标志着 Java 平台的现代化。
通过本文提供的详细说明和代码示例,开发者可以深入理解这些特性并在项目中应用它们。建议开发者尝试这些新特性,并在迁移到 Java 17 时注意兼容性问题。更多详细信息可参考 Oracle JDK 17 发布说明 和 Baeldung Java 17 新特性。