JAVA版本及其新功能和特性
2022-09-29 13:51:45 0 举报
AI智能生成
JAVA版本及其新功能和特性
作者其他创作
大纲/内容
9
2017 年 9 月 22 日
新特性
模块系统
JPMS(Java Platform Module System)是Java 9发行版的核心亮点。它也被称为Jigshaw项目。模块是新的结构,就像我们已经有包一样。使用新的模块化编程开发的应用程序可以看作是交互模块的集合,这些模块之间具有明确定义的边界和依赖关系。
JPMS包括为编写模块化应用程序提供支持,以及将JDK源代码模块化。JDK 9 附带了大约 92 个模块(在 GA 版本中可以进行更改)。Java 9 Module System有一个"java.base"模块。它被称为基本模块。它是一个独立的模块,不依赖于任何其他模块。默认情况下,所有其他模块都依赖于"java.base"。
在java模块化编程中:
一个模块通常只是一个 jar 文件,在根目录下有一个文件module-info.class。
要使用模块,请将 jar 文件包含到modulepath而不是classpath. 添加到类路径的模块化 jar 文件是普通的 jar 文件,module-info.class文件将被忽略。
典型的module-info.java类如下所示:
module helloworld {
exports cn.dayangshuo.demo;
}
module test {
requires helloworld;
}
JPMS包括为编写模块化应用程序提供支持,以及将JDK源代码模块化。JDK 9 附带了大约 92 个模块(在 GA 版本中可以进行更改)。Java 9 Module System有一个"java.base"模块。它被称为基本模块。它是一个独立的模块,不依赖于任何其他模块。默认情况下,所有其他模块都依赖于"java.base"。
在java模块化编程中:
一个模块通常只是一个 jar 文件,在根目录下有一个文件module-info.class。
要使用模块,请将 jar 文件包含到modulepath而不是classpath. 添加到类路径的模块化 jar 文件是普通的 jar 文件,module-info.class文件将被忽略。
典型的module-info.java类如下所示:
module helloworld {
exports cn.dayangshuo.demo;
}
module test {
requires helloworld;
}
JShell – REPL 工具
JShell是JDK 9发行版 [ JEP 222 ]附带的新命令行交互式工具,用于评估用 Java 编写的声明、语句和表达式。JShell 允许我们执行 Java 代码片段并立即获得结果,而无需创建解决项目。
Jshell 很像我们在 linux 操作系统中的命令窗口。不同之处在于 JShell 是特定于 Java 的。除了执行简单的代码片段之外,它还有许多其他功能,例如:
在单独的窗口中启动内置代码编辑器
在单独的窗口中启动你选择的代码编辑器
在这些外部编辑器中发生保存操作时执行代码
从文件系统加载预先编写的类
Jshell 很像我们在 linux 操作系统中的命令窗口。不同之处在于 JShell 是特定于 Java 的。除了执行简单的代码片段之外,它还有许多其他功能,例如:
在单独的窗口中启动内置代码编辑器
在单独的窗口中启动你选择的代码编辑器
在这些外部编辑器中发生保存操作时执行代码
从文件系统加载预先编写的类
HTTP 2 客户端
HTTP/1.1 客户端于 1997 年发布。此后发生了很多变化。因此,Java 9 引入了新的 API,它使用起来更干净、更清晰,并且还增加了对 HTTP/2 的支持。新 API 使用 3 个主要类HttpClient,HttpRequest和HttpResponse.
要发出请求,就像获取客户端、构建请求并发送它一样简单,示例:
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder().uri(new URI("https://baidu.com/")).GET().build();
HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandler.asString());
System.out.println(httpResponse.body());
上面的代码看起来更清晰易读。
httpClient.sendAsync()API 还支持使用方法的异步 HTTP 请求。它返回CompletableFuture可用于确定请求是否已完成。它还可以在HttpResponse请求完成后访问。如果你愿意,甚至可以在请求完成之前取消它。
示例:
if(httpResponse.isDone()) {
System.out.println(httpResponse.get().statusCode());
System.out.println(httpResponse.get().body());
} else {
httpResponse.cancel(true);
}
要发出请求,就像获取客户端、构建请求并发送它一样简单,示例:
HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder().uri(new URI("https://baidu.com/")).GET().build();
HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandler.asString());
System.out.println(httpResponse.body());
上面的代码看起来更清晰易读。
httpClient.sendAsync()API 还支持使用方法的异步 HTTP 请求。它返回CompletableFuture可用于确定请求是否已完成。它还可以在HttpResponse请求完成后访问。如果你愿意,甚至可以在请求完成之前取消它。
示例:
if(httpResponse.isDone()) {
System.out.println(httpResponse.get().statusCode());
System.out.println(httpResponse.get().body());
} else {
httpResponse.cancel(true);
}
改进的 Javadoc
Java 9 增强了javadoc生成 HTML5 标记的工具。它目前以 HTML 4.01 生成页面。
为了生成 HTML5 Javadoc,参数-html5需要放在命令行参数中。要在命令行上生成文档,你将运行:
javadoc [选项] [包名] [源文件] [@files]
使用 HTML5 可以带来更简单的 HTML5 结构的好处。它还实现了可访问性的WAI-ARIA 标准。这旨在使身体或视觉障碍的人更容易使用屏幕阅读器等工具访问 javadocs 页面。
JEP 225提供了在 javadoc 中搜索程序元素和标记的单词和短语的能力。
以下内容将被索引和搜索:
模块的声明名称
套餐
类型和成员
方法参数类型的简单名称
这是在客户端实现的,带有一个新的search.jsJavascript 文件,以及生成 javadoc 时生成的索引。生成的 HTML5 API 页面上有一个搜索框。
请注意,默认情况下会添加搜索选项,但可以使用参数关闭:-noindex。
为了生成 HTML5 Javadoc,参数-html5需要放在命令行参数中。要在命令行上生成文档,你将运行:
javadoc [选项] [包名] [源文件] [@files]
使用 HTML5 可以带来更简单的 HTML5 结构的好处。它还实现了可访问性的WAI-ARIA 标准。这旨在使身体或视觉障碍的人更容易使用屏幕阅读器等工具访问 javadocs 页面。
JEP 225提供了在 javadoc 中搜索程序元素和标记的单词和短语的能力。
以下内容将被索引和搜索:
模块的声明名称
套餐
类型和成员
方法参数类型的简单名称
这是在客户端实现的,带有一个新的search.jsJavascript 文件,以及生成 javadoc 时生成的索引。生成的 HTML5 API 页面上有一个搜索框。
请注意,默认情况下会添加搜索选项,但可以使用参数关闭:-noindex。
多版本兼容 JAR 包
此增强与如何将应用程序类打包到 jar 文件中有关。以前,开发者必须将所有类打包到一个 jar 文件中,然后放入希望使用它的另一个应用程序的类路径中。
使用多版本特性,现在一个 jar 可以包含一个类的不同版本——兼容不同的 JDK 版本。关于一个类的不同版本,以及加载的类应该选择哪个类的JDK版本的信息存储在MANIFEST.MF文件中。在这种情况下,文件在其主要部分中MANIFEST.MF包含该条目Multi-Release: true
此外,META-INF 包含一个版本子目录,其以整数命名的子目录——从 9 开始(对于 Java 9)——存储特定于版本的类和资源文件。例如
JAR content root
A.class
B.class
C.class
D.class
META-INF
MANIFEST.MF
versions
9
A.class
B.class
假设在 JDK 10 A.class中更新为利用一些 Java 10 新特性,那么这个 Jar 文件可以像这样更新:
JAR content root
A.class
B.class
C.class
D.class
META-INF
MANIFEST.MF
versions
9
A.class
B.class
10
A.class
它看起来非常有希望解决在大型应用程序中经常看到的依赖地狱,其中不同版本的 jar 彼此不兼容。此功能可以为解决这些情况提供很大帮助。
使用多版本特性,现在一个 jar 可以包含一个类的不同版本——兼容不同的 JDK 版本。关于一个类的不同版本,以及加载的类应该选择哪个类的JDK版本的信息存储在MANIFEST.MF文件中。在这种情况下,文件在其主要部分中MANIFEST.MF包含该条目Multi-Release: true
此外,META-INF 包含一个版本子目录,其以整数命名的子目录——从 9 开始(对于 Java 9)——存储特定于版本的类和资源文件。例如
JAR content root
A.class
B.class
C.class
D.class
META-INF
MANIFEST.MF
versions
9
A.class
B.class
假设在 JDK 10 A.class中更新为利用一些 Java 10 新特性,那么这个 Jar 文件可以像这样更新:
JAR content root
A.class
B.class
C.class
D.class
META-INF
MANIFEST.MF
versions
9
A.class
B.class
10
A.class
它看起来非常有希望解决在大型应用程序中经常看到的依赖地狱,其中不同版本的 jar 彼此不兼容。此功能可以为解决这些情况提供很大帮助。
注释SafeVarargs范围的延伸
直到Java 8,@SafeVarargs才能在静态方法、final方法和构造器上使用。但是这些方法或者构造器是不能被覆盖的。这些方法中缺少另一个不能被覆盖的方法,这个方法就是私有方法。Java 9可以将@SafeVarargs添加到私有方法上。下面的例子在Java 9中是正确的,但是在Java 8中就会抛出编译时错误: 注释@SafeVarargs不能在非final的实例方法iAmSafeVaragrsMethod上使用。
集合工厂方法
List,Set 和 Map 接口中,新的静态工厂方法可以创建这些集合的不可变实例。
私有接口方法
Java 8 允许在接口中编写默认方法,这是一个广受欢迎的功能。从 Java 9 开始,您可以在接口中包含私有方法。
这些私有方法将提高接口内部的代码可重用性。例如,如果两个默认方法需要共享代码,私有接口方法将允许它们这样做,但不会将该私有方法暴露给它的实现类。
在接口中使用私有方法有四个规则:
私有接口方法不能是抽象的。
私有方法只能在接口内部使用。
私有静态方法可以在其他静态和非静态接口方法中使用。
私有非静态方法不能在私有静态方法中使用。
示例:
public interface CustomCalculator {
default int addEvenNumbers(int... nums) {
return add(n -> n % 2 == 0, nums);
}
default int addOddNumbers(int... nums) {
return add(n -> n % 2 != 0, nums);
}
private int add(IntPredicate predicate, int... nums) {
return IntStream.of(nums)
.filter(predicate)
.sum();
}
}
这些私有方法将提高接口内部的代码可重用性。例如,如果两个默认方法需要共享代码,私有接口方法将允许它们这样做,但不会将该私有方法暴露给它的实现类。
在接口中使用私有方法有四个规则:
私有接口方法不能是抽象的。
私有方法只能在接口内部使用。
私有静态方法可以在其他静态和非静态接口方法中使用。
私有非静态方法不能在私有静态方法中使用。
示例:
public interface CustomCalculator {
default int addEvenNumbers(int... nums) {
return add(n -> n % 2 == 0, nums);
}
default int addOddNumbers(int... nums) {
return add(n -> n % 2 != 0, nums);
}
private int add(IntPredicate predicate, int... nums) {
return IntStream.of(nums)
.filter(predicate)
.sum();
}
}
进程 API
在 Java 5 之前,生成新进程的唯一方法是使用该Runtime.getRuntime().exec()方法。然后在 Java 5 中,ProcessBuilder引入了 API,它支持一种更简洁的方式来生成新进程。现在,Java 9 添加了一种获取有关当前进程和任何衍生进程的信息的新方法。
要获取任何进程的信息,现在您应该使用java.lang.ProcessHandle.Info接口。此界面可用于获取大量信息,例如:
用于启动进程的命令
命令的参数
进程开始的时刻
它和创建它的用户花费的总时间
ProcessHandle processHandle = ProcessHandle.current();
ProcessHandle.Info processInfo = processHandle.info();
System.out.println(processHandle.getPid());
System.out.println(processInfo.arguments().isPresent());
System.out.println(pprocessInfo.command().isPresent());
System.out.println(processInfo.command().get().contains("java"));
System.out.println(processInfo.startInstant().isPresent());
要获取新衍生进程的信息,请使用process.toHandle()方法获取ProcessHandle实例。其余一切如上。
String javaPrompt = ProcessUtils.getJavaCmd().getAbsolutePath();
ProcessBuilder processBuilder = new ProcessBuilder(javaPrompt, "-version");
Process process = processBuilder.inheritIO().start();
ProcessHandle processHandle = process.toHandle();
也用于ProcessHandle.allProcesses()获取系统中所有可用进程的 ProcessHandle 流。
要获取所有子进程的列表(直接以及 n 级深度),请使用children()和descendants()方法。
Stream<ProcessHandle> children = ProcessHandle.current().children();
Stream<ProcessHandle> descendants = ProcessHandle.current().descendants();
要获取任何进程的信息,现在您应该使用java.lang.ProcessHandle.Info接口。此界面可用于获取大量信息,例如:
用于启动进程的命令
命令的参数
进程开始的时刻
它和创建它的用户花费的总时间
ProcessHandle processHandle = ProcessHandle.current();
ProcessHandle.Info processInfo = processHandle.info();
System.out.println(processHandle.getPid());
System.out.println(processInfo.arguments().isPresent());
System.out.println(pprocessInfo.command().isPresent());
System.out.println(processInfo.command().get().contains("java"));
System.out.println(processInfo.startInstant().isPresent());
要获取新衍生进程的信息,请使用process.toHandle()方法获取ProcessHandle实例。其余一切如上。
String javaPrompt = ProcessUtils.getJavaCmd().getAbsolutePath();
ProcessBuilder processBuilder = new ProcessBuilder(javaPrompt, "-version");
Process process = processBuilder.inheritIO().start();
ProcessHandle processHandle = process.toHandle();
也用于ProcessHandle.allProcesses()获取系统中所有可用进程的 ProcessHandle 流。
要获取所有子进程的列表(直接以及 n 级深度),请使用children()和descendants()方法。
Stream<ProcessHandle> children = ProcessHandle.current().children();
Stream<ProcessHandle> descendants = ProcessHandle.current().descendants();
Collection(集合) API更新
从 Java 9 开始,您可以使用新的工厂方法创建不可变集合,例如不可变list、不可变set和不可变map。例如:
import java.util.List;
public class ImmutableCollections {
public static void main(String[] args) {
List<String> namesList = List.of("Lokesh", "Amit", "John");
Set<String> namesSet = Set.of("Lokesh", "Amit", "John");
Map<String, String> namesMap = Map.ofEntries(
Map.entry("1", "Lokesh"),
Map.entry("2", "Amit"),
Map.entry("3", "Brian"));
}
}
import java.util.List;
public class ImmutableCollections {
public static void main(String[] args) {
List<String> namesList = List.of("Lokesh", "Amit", "John");
Set<String> namesSet = Set.of("Lokesh", "Amit", "John");
Map<String, String> namesMap = Map.ofEntries(
Map.entry("1", "Lokesh"),
Map.entry("2", "Amit"),
Map.entry("3", "Brian"));
}
}
Stream(流) API改进
Java 9 引入了两种与 Streams 交互的新方法,即takeWhile/dropWhile方法。此外,它还添加了两个重载方法,即ofNullable和iterate方法。
新方法takeWhile。dropWhile允许开发者基于谓词获取流的一部分
在有序流上,takeWhile返回从流中获取的与给定谓词匹配的元素的“最长前缀”,从流的开头开始。dropWhile返回与 不匹配的剩余项目takeWhile。
在无序流上,takeWhile返回与给定谓词(但不是全部)匹配的流元素的子集,从流的开头开始。dropWhile在删除与给定谓词匹配的元素子集后返回剩余的流元素。
List<String> alphabets = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i");
List<String> subset1 = alphabets
.stream()
.takeWhile(s -> !s.equals("d"))
.collect(Collectors.toList());
//打印出:[a, b, c]
System.out.println(subset1);
List<String> alphabets = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i");
List<String> subset2 = alphabets
.stream()
.dropWhile(s -> !s.equals("d"))
.collect(Collectors.toList());
//打印出:[d, e, f, g, h, i]
System.out.println(subset2);
同样,在 Java 8 之前,流中不能有null值。它会导致NullPointerException. 从 Java 9 开始,Stream.ofNullable()方法允许您创建一个单元素流,该流包装一个不为null的值,否则为空流。从技术上讲,Stream.ofNullable()在流 API 的上下文中,与空条件检查非常相似。
新方法takeWhile。dropWhile允许开发者基于谓词获取流的一部分
在有序流上,takeWhile返回从流中获取的与给定谓词匹配的元素的“最长前缀”,从流的开头开始。dropWhile返回与 不匹配的剩余项目takeWhile。
在无序流上,takeWhile返回与给定谓词(但不是全部)匹配的流元素的子集,从流的开头开始。dropWhile在删除与给定谓词匹配的元素子集后返回剩余的流元素。
List<String> alphabets = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i");
List<String> subset1 = alphabets
.stream()
.takeWhile(s -> !s.equals("d"))
.collect(Collectors.toList());
//打印出:[a, b, c]
System.out.println(subset1);
List<String> alphabets = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i");
List<String> subset2 = alphabets
.stream()
.dropWhile(s -> !s.equals("d"))
.collect(Collectors.toList());
//打印出:[d, e, f, g, h, i]
System.out.println(subset2);
同样,在 Java 8 之前,流中不能有null值。它会导致NullPointerException. 从 Java 9 开始,Stream.ofNullable()方法允许您创建一个单元素流,该流包装一个不为null的值,否则为空流。从技术上讲,Stream.ofNullable()在流 API 的上下文中,与空条件检查非常相似。
改进 try-with-resources
如果你已经有一个资源是 final 或等效于 final 变量,您可以在 try-with-resources 语句中使用该变量,而无需在 try-with-resources 语句中声明一个新变量。
改进的弃用注解 @Deprecated
从 Java 9 开始,@Deprecated注解将具有两个属性,即forRemoval和since.
forRemoval – 指示带注释的元素是否会在未来版本中被删除。
since - 它返回注释元素被弃用的版本。
强烈建议使用 @deprecated javadoc 标记在文档中解释弃用程序元素的原因。如果适用,文档还应建议并链接到推荐的替代 API。替换 API 通常具有微妙不同的语义,因此也应讨论此类问题。
forRemoval – 指示带注释的元素是否会在未来版本中被删除。
since - 它返回注释元素被弃用的版本。
强烈建议使用 @deprecated javadoc 标记在文档中解释弃用程序元素的原因。如果适用,文档还应建议并链接到推荐的替代 API。替换 API 通常具有微妙不同的语义,因此也应讨论此类问题。
改进钻石操作符(Diamond Operator)
匿名类可以使用钻石操作符(Diamond Operator)。
改进 Optional 类
java.util.Optional 添加了很多新的有用方法,Optional 可以直接转为 stream。
多分辨率图像 API
定义多分辨率图像API,开发者可以很容易的操作和展示不同分辨率的图像了。
改进的 CompletableFuture API
CompletableFuture 类的异步机制可以在 ProcessHandle.onExit 方法退出时执行操作。
轻量级的 JSON API
内置了一个轻量级的JSON API
响应式流(Reactive Streams) API
Java 9中引入了新的响应式流 API 来支持 Java 9 中的响应式编程。
平台和 JVM 日志记录
JDK 9 通过新的日志记录 API 改进了平台类(JDK 类)和 JVM 组件中的日志记录。它允许开发者指定自己选择的日志记录框架(例如Log4J2)作为日志记录工具,用于记录来自 JDK 类的消息。
关于这个 API,你应该知道几件事:
API 旨在供 JDK 中的类使用,而不是由应用程序类使用。
对于应用程序代码,开发者将像以前一样继续使用其他日志记录 API。
API 不允许开发者以编程方式配置记录器。
API 由以下部分组成:
服务接口,java.lang.System.LoggerFinder是一个抽象静态类
java.lang.System.Logger提供日志记录 API 的接口
getLogger()类中的一个重载方法java.lang.System,它返回一个记录器实例。
JDK 9 还添加了一个新的命令行选项 ,-Xlog它使开发者可以单点访问从 JVM 的所有类记录的所有消息。以下是使用该-Xlog选项的语法:
-Xlog[:][:[][:[][:]]]
所有选项都是可选的。如果缺少前面的部分,-Xlog则必须为该部分使用冒号。例如,-Xlog::stderr表示所有部分都是默认的,输出设置为stderr.
关于这个 API,你应该知道几件事:
API 旨在供 JDK 中的类使用,而不是由应用程序类使用。
对于应用程序代码,开发者将像以前一样继续使用其他日志记录 API。
API 不允许开发者以编程方式配置记录器。
API 由以下部分组成:
服务接口,java.lang.System.LoggerFinder是一个抽象静态类
java.lang.System.Logger提供日志记录 API 的接口
getLogger()类中的一个重载方法java.lang.System,它返回一个记录器实例。
JDK 9 还添加了一个新的命令行选项 ,-Xlog它使开发者可以单点访问从 JVM 的所有类记录的所有消息。以下是使用该-Xlog选项的语法:
-Xlog[:][:[][:[][:]]]
所有选项都是可选的。如果缺少前面的部分,-Xlog则必须为该部分使用冒号。例如,-Xlog::stderr表示所有部分都是默认的,输出设置为stderr.
10
2018年3月21日
新特性
var关键字(局部变量类型推断)
Java 现在有var声明。它允许开发者在不指定其类型的情况下声明局部变量。变量的类型将从创建的实际对象的类型中推断出来。
垃圾收集器接口
在早期的 JDK 结构中,组成垃圾收集器 (GC) 实现的组件分散在代码库的各个部分。它在 Java 10 中发生了变化。现在,它是 JVM 源代码中的一个干净的接口,允许快速轻松地集成替代收集器。它将改善不同垃圾收集器的源代码隔离。
G1 的并行全 GC
Java 9 引入了 G1(垃圾优先)垃圾收集器。G1 垃圾收集器旨在避免完全收集,但是当并发收集无法足够快地回收内存时。通过此更改,将发生回退完整 GC。G1 的完整 GC 的当前实现使用单线程标记-扫描-紧凑算法。此更改将并行化 mark-sweep-compact 算法并使用相同数量的线程。当收集的并发线程无法足够快地恢复内存时,它将被触发。线程数可以通过-XX:ParallelGCThreads选项来控制。
替代存储设备上的堆分配
此更改的目标是使 HotSpot VM 能够在用户指定的备用内存设备(例如 NV-DIMM)上分配 Java 对象堆。
要在此类内存中分配堆,我们可以添加一个新选项-XX:AllocateHeapAt=<path>. 此选项将获取文件系统的路径并使用内存映射来实现在内存设备上分配对象堆的预期结果。现有的与堆相关的标志,如-Xmx、-Xms等,以及与垃圾收集相关的标志将继续像以前一样工作。
要在此类内存中分配堆,我们可以添加一个新选项-XX:AllocateHeapAt=<path>. 此选项将获取文件系统的路径并使用内存映射来实现在内存设备上分配对象堆的预期结果。现有的与堆相关的标志,如-Xmx、-Xms等,以及与垃圾收集相关的标志将继续像以前一样工作。
将 JDK库整合到单个存储库中
作为此更改的一部分,JDK 库的众多存储库被合并到一个存储库中,以简化和简化开发。
在 JDK 9 中有八个存储库:root、corba、hotspot、jaxp、jaxws、jdk、langtools和nashorn. 在统一的林中,Java 模块的代码通常组合在一个顶级src目录下。例如,今天在 JDK 林中有基于模块的目录,如
$ROOT/jdk/src/java.base
...
$ROOT/langtools/src/java.compiler
...
在合并的库中,此代码改为组织为-
$ROOT/src/java.base
$ROOT/src/java.compiler
...
在 JDK 9 中有八个存储库:root、corba、hotspot、jaxp、jaxws、jdk、langtools和nashorn. 在统一的林中,Java 模块的代码通常组合在一个顶级src目录下。例如,今天在 JDK 林中有基于模块的目录,如
$ROOT/jdk/src/java.base
...
$ROOT/langtools/src/java.compiler
...
在合并的库中,此代码改为组织为-
$ROOT/src/java.base
$ROOT/src/java.compiler
...
应用程序类-数据共享
此功能的目标是改善启动足迹,扩展现有的类数据共享 (“CDS”) 功能以允许将应用程序类放置在共享存档中。
JDK 5 中引入的类数据共享允许将一组类预处理为共享存档文件,然后可以在运行时进行内存映射以减少启动时间。当多个 JVM 共享同一个归档文件时,它还可以减少动态内存占用。
目前 CDS 只允许引导类加载器加载归档类。应用程序 CDS 允许内置系统类加载器、内置平台类加载器和自定义类加载器加载归档类。
指定-XX:+UseAppCDS命令行选项以启用系统类加载器、平台类加载器和其他用户定义的类加载器的类数据共享。
JDK 5 中引入的类数据共享允许将一组类预处理为共享存档文件,然后可以在运行时进行内存映射以减少启动时间。当多个 JVM 共享同一个归档文件时,它还可以减少动态内存占用。
目前 CDS 只允许引导类加载器加载归档类。应用程序 CDS 允许内置系统类加载器、内置平台类加载器和自定义类加载器加载归档类。
指定-XX:+UseAppCDS命令行选项以启用系统类加载器、平台类加载器和其他用户定义的类加载器的类数据共享。
根证书
cacerts 密钥库是 JDK 的一部分,旨在包含一组根证书,可用于在各种安全协议中使用的证书链中建立信任。然而,JDK 源代码中的 cacerts 密钥库目前是空的。cacerts 密钥库将填充一组由 Oracle 的 Java SE 根 CA 程序的 CA 颁发的根证书。许多供应商已经签署了所需的协议,并且每个供应商都将包含一份根证书列表。未签署协议的将不包括在内。那些需要更长时间处理的将包含在下一个版本中。这也意味着 Oracle 和 Open JDK 二进制文件在功能上是相同的。TLS 等关键安全组件将默认在未来的 OpenJDK 构建中工作。
基于 Java 的 JIT 编译器(实验性)
此功能使基于 Java 的 JIT 编译器Graal可以用作 Linux/x64 平台上的实验性 JIT 编译器。Graal 将使用 JDK 9 中引入的 JVM 编译器接口 (JVMCI)。Graal 已经在 JDK 中,因此将其作为实验性 JIT 启用将主要是测试和调试工作。
要启用 Graal 作为 JIT 编译器,请在 java 命令行上使用以下选项:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Graal 是从头开始用 Java 完全重写 JIT 编译器。以前的 JIT 编译器是用 C++ 编写的。
要启用 Graal 作为 JIT 编译器,请在 java 命令行上使用以下选项:
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler
Graal 是从头开始用 Java 完全重写 JIT 编译器。以前的 JIT 编译器是用 C++ 编写的。
Thread-Local握手
这个 JEP 为提高 VM 性能奠定了基础,它可以在应用程序线程上执行回调,而无需执行全局 VM 安全点。这意味着 JVM 可以停止单个线程,而不仅仅是所有线程。线程本地握手最初将在 x64 和 SPARC 上实现。其他平台将退回到正常的安全点。
一个新的产品选项-XX:ThreadLocalHandshakes(默认值true)允许用户在支持的平台上选择正常的安全点。
一个新的产品选项-XX:ThreadLocalHandshakes(默认值true)允许用户在支持的平台上选择正常的安全点。
11
2018 年 9 月
新特性
HTTP客户端API
Java 使用HttpURLConnection进行HTTP通信已经很长一段时间了。但随着时间的推移,要求变得越来越复杂,应用程序的要求也越来越高。在 Java 11 之前,开发人员不得不求助于功能丰富的库,如Apache HttpComponents或OkHttp等。
我们看到Java 9发布包含一个HttpClient实现作为实验性功能。它随着时间的推移而发展,现在是 Java 11 的最终功能。现在 Java 应用程序可以进行 HTTP 通信,而无需任何外部依赖。
我们看到Java 9发布包含一个HttpClient实现作为实验性功能。它随着时间的推移而发展,现在是 Java 11 的最终功能。现在 Java 应用程序可以进行 HTTP 通信,而无需任何外部依赖。
无需编译即可启动单文件程序
传统上,对于我们想要执行的每个程序,我们都需要先编译它。对于用于测试目的的小程序来说,这似乎是不必要的冗长过程。
Java 11 改变了它,现在我们可以执行包含在单个文件中的 Java 源代码,而无需先编译它。
$java HelloWorld.java
Java 11 改变了它,现在我们可以执行包含在单个文件中的 Java 源代码,而无需先编译它。
$java HelloWorld.java
字符串API更新
String.repeat(Integer)
String.isBlank()
String.strip()
String.lines()
String.isBlank()
String.strip()
String.lines()
Collection.toArray
在 Java 11 之前,将集合转换为数组并不简单。Java 11 使转换更加方便。
public class HelloWorld {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("alex");
names.add("brian");
names.add("charles");
String[] namesArr1 = names.toArray(new String[names.size()]); //Java 11之前
String[] namesArr2 = names.toArray(String[]::new); //Java 11
}
}
public class HelloWorld {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("alex");
names.add("brian");
names.add("charles");
String[] namesArr1 = names.toArray(new String[names.size()]); //Java 11之前
String[] namesArr2 = names.toArray(String[]::new); //Java 11
}
}
Files.readString() 和 Files.writeString()
使用这些重载方法,Java 11 旨在减少大量样板代码,从而更容易读取和写入文件。
public class HelloWorld {
public static void main(String[] args) {
//读取文件内容为字符串
URI txtFileUri = getClass().getClassLoader().getResource("helloworld.txt").toURI();
String content = Files.readString(Path.of(txtFileUri),Charset.defaultCharset());
//把字符串写入文件
Path tmpFilePath = Path.of(File.createTempFile("tempFile", ".tmp").toURI());
Path returnedFilePath = Files.writeString(tmpFilePath,"Hello World!",
Charset.defaultCharset(), StandardOpenOption.WRITE);
}
}
public class HelloWorld {
public static void main(String[] args) {
//读取文件内容为字符串
URI txtFileUri = getClass().getClassLoader().getResource("helloworld.txt").toURI();
String content = Files.readString(Path.of(txtFileUri),Charset.defaultCharset());
//把字符串写入文件
Path tmpFilePath = Path.of(File.createTempFile("tempFile", ".tmp").toURI());
Path returnedFilePath = Files.writeString(tmpFilePath,"Hello World!",
Charset.defaultCharset(), StandardOpenOption.WRITE);
}
}
Optional.isEmpty()
Optional是一个容器对象,它可能包含也可能不包含非空值。如果不存在任何值,则该对象被认为是空的。
isPresent()方法如果值存在则返回true,否则返回false。
isEmpty()方法与isPresent()方法相反,如果存在值则返回false,否则返回true。
所以我们无论如何都不要写否定条件。适当时使用这两种方法中的任何一种。
public class HelloWorld {
public static void main(String[] args) {
String currentTime = null;
assertTrue(!Optional.ofNullable(currentTime).isPresent());
assertTrue(Optional.ofNullable(currentTime).isEmpty());
currentTime = "12:00 PM";
assertFalse(!Optional.ofNullable(currentTime).isPresent());
assertFalse(Optional.ofNullable(currentTime).isEmpty());
}
}
isPresent()方法如果值存在则返回true,否则返回false。
isEmpty()方法与isPresent()方法相反,如果存在值则返回false,否则返回true。
所以我们无论如何都不要写否定条件。适当时使用这两种方法中的任何一种。
public class HelloWorld {
public static void main(String[] args) {
String currentTime = null;
assertTrue(!Optional.ofNullable(currentTime).isPresent());
assertTrue(Optional.ofNullable(currentTime).isEmpty());
currentTime = "12:00 PM";
assertFalse(!Optional.ofNullable(currentTime).isPresent());
assertFalse(Optional.ofNullable(currentTime).isEmpty());
}
}
12
2019 年 3 月 19 日
新特性
Stream API 中的 Collectors.teeing()
teeing 收集器已公开为静态方法Collectors::teeing。该收集器将其输入转发给其他两个收集器,然后将它们的结果使用函数合并。
teeing(Collector, Collector, BiFunction)接受两个收集器和一个合并其结果的函数。传递给结果收集器的每个元素都由两个下游收集器处理,然后使用指定的合并函数将它们的结果合并到最终结果中。
teeing(Collector, Collector, BiFunction)接受两个收集器和一个合并其结果的函数。传递给结果收集器的每个元素都由两个下游收集器处理,然后使用指定的合并函数将它们的结果合并到最终结果中。
字符串 API 变更
字符串缩进
transform
字符串常量
Files.mismatch(path, path)
有时,我们想确定两个文件是否具有相同的内容。此 API 有助于比较文件的内容。
mismatch()方法比较两个文件路径并返回一个long值。long 表示两个文件内容中第一个不匹配字节的位置。'–1'如果文件“相等”,则返回值。
Path helloworld1 = tempDir.resolve("helloworld1.txt");
Path helloworld2 = tempDir.resolve("helloworld2.txt");
long diff = Files.mismatch(helloworld1, helloworld2);
mismatch()方法比较两个文件路径并返回一个long值。long 表示两个文件内容中第一个不匹配字节的位置。'–1'如果文件“相等”,则返回值。
Path helloworld1 = tempDir.resolve("helloworld1.txt");
Path helloworld2 = tempDir.resolve("helloworld2.txt");
long diff = Files.mismatch(helloworld1, helloworld2);
紧凑数字格式
由用户界面或命令行工具呈现的大量数字总是难以解析。使用数字的缩写形式更为常见。紧凑的数字表示更容易阅读并且在屏幕上需要更少的空间而不会失去原始含义。
例如3.6 M比3,600,000更容易阅读。
Java 12 引入了一种名为NumberFormat.getCompactNumberInstance(Locale, NumberFormat.Style)的便捷方法,用于创建紧凑的数字表示。
NumberFormat formatter = NumberFormat.getCompactNumberInstance(Locale.US,NumberFormat.Style.SHORT);
String formattedString = formatter.format(25000L); ``//25K
例如3.6 M比3,600,000更容易阅读。
Java 12 引入了一种名为NumberFormat.getCompactNumberInstance(Locale, NumberFormat.Style)的便捷方法,用于创建紧凑的数字表示。
NumberFormat formatter = NumberFormat.getCompactNumberInstance(Locale.US,NumberFormat.Style.SHORT);
String formattedString = formatter.format(25000L); ``//25K
支持 Unicode 11
13
2019 年 9 月 17 日
新特性
switch表达式改进
使用yield,我们现在可以有效地从 switch 表达式返回值,并能够更容易实现策略模式。
package cn.dayangshuo.collectors;
/**
* @author DAYANG
*/
public class SwitchTest {
public static void main(String[] args) {
var me = 4;
var operation = "平方";
var result = switch (operation) {
case "加倍" -> {
yield me * 2;
}
case "平方" -> {
yield me * me;
}
default -> me;
};
System.out.println(result);
}
}
package cn.dayangshuo.collectors;
/**
* @author DAYANG
*/
public class SwitchTest {
public static void main(String[] args) {
var me = 4;
var operation = "平方";
var result = switch (operation) {
case "加倍" -> {
yield me * 2;
}
case "平方" -> {
yield me * me;
}
default -> me;
};
System.out.println(result);
}
}
文本块改进
第二个预览功能是多行String的文本块,例如嵌入的 JSON、XML、HTML 等。
早些时候,为了在我们的代码中嵌入 JSON,我们将其声明为字符串文字:
String JSON_STRING = "{\r\n" + "\"name\" : \"Baeldung\",\r\n" + "\"website\" : \"https://www.dayangshuo.cn/\"\r\n" + "}";
现在让我们使用字符串文本块编写相同的 JSON :
String TEXT_BLOCK_JSON = """
{
"name" : "Baeldung",
"website" : "https://www.dayangshuo.cn/"
}
""";
很明显,不需要转义双引号或添加回车。通过使用文本块,嵌入的 JSON 更易于编写,更易于阅读和维护。
早些时候,为了在我们的代码中嵌入 JSON,我们将其声明为字符串文字:
String JSON_STRING = "{\r\n" + "\"name\" : \"Baeldung\",\r\n" + "\"website\" : \"https://www.dayangshuo.cn/\"\r\n" + "}";
现在让我们使用字符串文本块编写相同的 JSON :
String TEXT_BLOCK_JSON = """
{
"name" : "Baeldung",
"website" : "https://www.dayangshuo.cn/"
}
""";
很明显,不需要转义双引号或添加回车。通过使用文本块,嵌入的 JSON 更易于编写,更易于阅读和维护。
动态CDS档案
一段时间以来,类数据共享 (CDS) 一直是 Java HotSpot VM 的一个突出特性。它允许在不同的 JVM 之间共享类元数据,以减少启动时间和内存占用。JDK 10 通过添加应用程序 CDS ( AppCDS ) 扩展了这种能力——让开发人员能够在共享存档中包含应用程序类。JDK 12 进一步增强了这个特性,默认包括 CDS 归档。
但是,归档应用程序类的过程很繁琐。要生成存档文件,开发人员必须先试运行他们的应用程序以创建类列表,然后将其转储到存档中。之后,该存档可用于在 JVM 之间共享元数据。
通过动态归档,JDK 13 简化了这个过程。现在我们可以在应用程序退出时生成一个共享存档。这消除了试运行的需要。
要使应用程序能够在默认系统存档之上创建动态共享存档,我们需要添加一个选项-XX:ArchiveClassesAtExit并将存档名称指定为参数:
java -XX:ArchiveClassesAtExit=<archive filename> -cp <app jar> AppName
然后,我们可以使用新创建的存档通过-XX:SharedArchiveFile选项运行相同的应用程序:
java -XX:SharedArchiveFile=<archive filename> -cp <app jar> AppName
但是,归档应用程序类的过程很繁琐。要生成存档文件,开发人员必须先试运行他们的应用程序以创建类列表,然后将其转储到存档中。之后,该存档可用于在 JVM 之间共享元数据。
通过动态归档,JDK 13 简化了这个过程。现在我们可以在应用程序退出时生成一个共享存档。这消除了试运行的需要。
要使应用程序能够在默认系统存档之上创建动态共享存档,我们需要添加一个选项-XX:ArchiveClassesAtExit并将存档名称指定为参数:
java -XX:ArchiveClassesAtExit=<archive filename> -cp <app jar> AppName
然后,我们可以使用新创建的存档通过-XX:SharedArchiveFile选项运行相同的应用程序:
java -XX:SharedArchiveFile=<archive filename> -cp <app jar> AppName
ZGC:取消提交未使用的内存
Java 11 中引入了ZGC作为低延迟垃圾收集机制,因此 GC 暂停时间从未超过 10 毫秒。但是,与 G1 和 Shenandoah 等其他 HotSpot VM GC 不同,它没有配备将未使用的堆内存返回给操作系统的功能。
我们现在减少了内存占用并提高了性能。
从 Java 13 开始,ZGC 现在默认将未提交的内存返回给操作系统,直到达到指定的最小堆大小。如果我们不想使用此功能,我们可以通过以下方式回到 Java 11 方式:
使用选项-XX:-ZUncommit,或
设置相等的最小 ( -Xms ) 和最大 ( -Xmx ) 堆大小
此外,ZGC 现在支持的最大堆大小为 16TB。早些时候,限制到4TB 。
我们现在减少了内存占用并提高了性能。
从 Java 13 开始,ZGC 现在默认将未提交的内存返回给操作系统,直到达到指定的最小堆大小。如果我们不想使用此功能,我们可以通过以下方式回到 Java 11 方式:
使用选项-XX:-ZUncommit,或
设置相等的最小 ( -Xms ) 和最大 ( -Xmx ) 堆大小
此外,ZGC 现在支持的最大堆大小为 16TB。早些时候,限制到4TB 。
重新实现 Legacy Socket API
自 Java 出现以来,我们已经将 Socket(java.net.Socket和java.net.ServerSocket)API 视为 Java 不可或缺的一部分。然而,在过去的二十年里,它们从未现代化。它们是用遗留的 Java 和 C 编写的,既麻烦又难以维护。
Java 13 逆势而上,取代了底层实现,使 API 与未来的用户模式线程保持一致。提供者接口现在指向NioSocketImpl而不是PlainSocketImpl。这个新编码的实现基于与java.nio相同的内部基础结构。
同样,我们确实有办法回到使用PlainSocketImpl*。我们可以通过将系统属性-Djdk.net.usePlainSocketImpl设置为true来启动 JVM,以使用较旧的实现。默认值为*NioSocketImpl。
Java 13 逆势而上,取代了底层实现,使 API 与未来的用户模式线程保持一致。提供者接口现在指向NioSocketImpl而不是PlainSocketImpl。这个新编码的实现基于与java.nio相同的内部基础结构。
同样,我们确实有办法回到使用PlainSocketImpl*。我们可以通过将系统属性-Djdk.net.usePlainSocketImpl设置为true来启动 JVM,以使用较旧的实现。默认值为*NioSocketImpl。
其他
java.nio –添加了方法*FileSystems.newFileSystem(Path, Map<String, ?>)
javax.crypto – 支持下一代 MS 密码术 (CNG)
javax.security – 添加属性jdk.sasl.disabledMechanisms以禁用 SASL 机制
javax.xml.crypto – 引入新的字符串常量来表示规范 XML 1.1 URI
javax.xml.parsers - 添加新方法以实例化具有命名空间支持的 DOM 和 SAX 工厂
Unicode 支持升级到版本 12.1
添加了对 Kerberos 主体名称规范化和跨领域引用的支持
javax.crypto – 支持下一代 MS 密码术 (CNG)
javax.security – 添加属性jdk.sasl.disabledMechanisms以禁用 SASL 机制
javax.xml.crypto – 引入新的字符串常量来表示规范 XML 1.1 URI
javax.xml.parsers - 添加新方法以实例化具有命名空间支持的 DOM 和 SAX 工厂
Unicode 支持升级到版本 12.1
添加了对 Kerberos 主体名称规范化和跨领域引用的支持
14
2020 年 3 月 17 日
新特性
NullPointerExceptions:精确描述哪个变量是null
package cn.dayangshuo.collectors;
/**
* @author DAYANG
*/
public class NullPointerExceprionsTest {
//空指针报错
public static void main(String[] args) {
User user = null;
System.out.println(user.getName());
}
static class User{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
//jdk8的报错信息:
Exception in thread "main" java.lang.NullPointerException
at cn.dayangshuo.collectors.NullPointerExceprionsTest.main(NullPointerExceprionsTest.java:9)
//jdk14的报错信息:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "cn.dayangshuo.collectors.NullPointerExceprionsTest$User.getName()" because "user" is null
at cn.dayangshuo.collectors.NullPointerExceprionsTest.main(NullPointerExceprionsTest.java:9)
/**
* @author DAYANG
*/
public class NullPointerExceprionsTest {
//空指针报错
public static void main(String[] args) {
User user = null;
System.out.println(user.getName());
}
static class User{
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
//jdk8的报错信息:
Exception in thread "main" java.lang.NullPointerException
at cn.dayangshuo.collectors.NullPointerExceprionsTest.main(NullPointerExceprionsTest.java:9)
//jdk14的报错信息:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "cn.dayangshuo.collectors.NullPointerExceprionsTest$User.getName()" because "user" is null
at cn.dayangshuo.collectors.NullPointerExceprionsTest.main(NullPointerExceprionsTest.java:9)
15
2020 年 9 月 15 日
新特性
EdDSA 算法
EdDSA (Edwards-Curve Digital Signature Algorithm) 是在 Java 15 中通过JEP 339添加的另一种附加数字签名方案。与其他可用的签名方案相比,它提供了更好的性能和安全的签名。
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.util.Base64;
/**
* @author DAYANG
*/
public class EdDSATest {
public static void main(String[] args) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair kp = kpg.generateKeyPair();
byte[] msg = "dayang".getBytes(StandardCharsets.UTF_8);
Signature sig = Signature.getInstance("Ed25519");
sig.initSign(kp.getPrivate());
sig.update(msg);
byte[] s = sig.sign();
String encodedString = Base64.getEncoder().encodeToString(s);
System.out.println(encodedString);
}
}
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.util.Base64;
/**
* @author DAYANG
*/
public class EdDSATest {
public static void main(String[] args) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair kp = kpg.generateKeyPair();
byte[] msg = "dayang".getBytes(StandardCharsets.UTF_8);
Signature sig = Signature.getInstance("Ed25519");
sig.initSign(kp.getPrivate());
sig.update(msg);
byte[] s = sig.sign();
String encodedString = Base64.getEncoder().encodeToString(s);
System.out.println(encodedString);
}
}
隐藏类
隐藏类不同于普通的 Java 类。它们不能被其他类的字节码直接使用。隐藏类旨在供在运行时生成类并通过反射间接使用它们的框架使用。
使用隐藏类,不是框架开发人员可以创建外部类看不到的不可发现的类;并且可以显式卸载,而不必担心来自其他类的可能引用。
JVM 支持主动卸载不可发现的类,因此框架可以灵活地定义任意数量的类。
使用隐藏类,不是框架开发人员可以创建外部类看不到的不可发现的类;并且可以显式卸载,而不必担心来自其他类的可能引用。
JVM 支持主动卸载不可发现的类,因此框架可以灵活地定义任意数量的类。
16
2021 年 3 月 16 日
新特性
时段支持
DateTimeFormatter的新增功能,可以表示一天中的时段,例如“上午”、“下午”
package cn.dayangshuo.collectors;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* @author DAYANG
*/
public class DateTimeTest {
public static void main(String[] args) {
String date1 = DateTimeFormatter.ofPattern("a").format(LocalTime.now());
String date2 = DateTimeFormatter.ofPattern("B").format(LocalTime.now());
String date3 = DateTimeFormatter.ofPattern("k").format(LocalTime.now());
System.out.println(date1);
System.out.println(date2);
System.out.println(date3);
}
}
//下午
//下午
//17
package cn.dayangshuo.collectors;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* @author DAYANG
*/
public class DateTimeTest {
public static void main(String[] args) {
String date1 = DateTimeFormatter.ofPattern("a").format(LocalTime.now());
String date2 = DateTimeFormatter.ofPattern("B").format(LocalTime.now());
String date3 = DateTimeFormatter.ofPattern("k").format(LocalTime.now());
System.out.println(date1);
System.out.println(date2);
System.out.println(date3);
}
}
//下午
//下午
//17
添加Stream.toList方法
现在我们可以使用Collectors.toList 和Collectors.toSet来减少一些模板代码:
List<String> integersAsString = Arrays.asList("1", "2", "3");
//之前这样写
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
//现在可以这样写
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();
List<String> integersAsString = Arrays.asList("1", "2", "3");
//之前这样写
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
//现在可以这样写
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();
instanceof的模式匹配
之前:
Object obj = "大阳";
if (obj instanceof String) {
String t = (String) obj;
// TODO
}
现在:
Object obj = "大阳";
if (obj instanceof String t) {
// TODO
}
Object obj = "大阳";
if (obj instanceof String) {
String t = (String) obj;
// TODO
}
现在:
Object obj = "大阳";
if (obj instanceof String t) {
// TODO
}
17(LTS)
2021年9月
新特性
record纪录类(标准版)
传统的Java应用程序通过创建一个类,通过该类的构造方法实例化类,并通过getter和setter方法访问成员变量或者设置成员变量的值。有了record关键字,你的代码会变得更加简洁。
package cn.dayangshuo;
/**
* 普通的dto
* @author DAYANG
*/
public class NormalEntity {
public static void main(String[] args) {
//传统的方式
User1 user1 = new User1("大阳", 1990);
System.out.println(user1);
//reocrd 记录类
User2 user2 = new User2("大阳", 1990);
System.out.println(user2);
}
}
/**
* 成员变量都是固定的
*/
class User1 {
String name;
Integer age;
public User1(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User1[" +
"name='" + name + '\'' +
", age=" + age +
']';
}
}
package cn.dayangshuo;
/**
* record 记录类
* 你也可以覆写equals() hashCode() toString()方法
* @author DAYANG
*/
record User2(String name, Integer age) {
@Override
public String toString() {
return "User2[" +
"name='" + name + '\'' +
", age=" + age +
']';
}
@Override
public boolean equals(Object obj) {
return false;
}
@Override
public int hashCode() {
return 0;
}
}
package cn.dayangshuo;
/**
* 普通的dto
* @author DAYANG
*/
public class NormalEntity {
public static void main(String[] args) {
//传统的方式
User1 user1 = new User1("大阳", 1990);
System.out.println(user1);
//reocrd 记录类
User2 user2 = new User2("大阳", 1990);
System.out.println(user2);
}
}
/**
* 成员变量都是固定的
*/
class User1 {
String name;
Integer age;
public User1(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User1[" +
"name='" + name + '\'' +
", age=" + age +
']';
}
}
package cn.dayangshuo;
/**
* record 记录类
* 你也可以覆写equals() hashCode() toString()方法
* @author DAYANG
*/
record User2(String name, Integer age) {
@Override
public String toString() {
return "User2[" +
"name='" + name + '\'' +
", age=" + age +
']';
}
@Override
public boolean equals(Object obj) {
return false;
}
@Override
public int hashCode() {
return 0;
}
}
新的字符串方法
从JDK8到JDK17陆续增加了更多字符串方法,操作字符串更简单了。
isBlank():如果字符串为空或字符串仅包含空格(包括制表符),则返回 true。注意与isEmpty() 不同,isEmpty()仅在长度为 0 时返回 true。
lines():将字符串拆分为字符串流,每个字符串包含一行。
strip() : 分别从开头和结尾;
stripLeading()/stripTrailing()仅开始和仅结束删除空格。
repeat(int times):返回一个字符串,该字符串采用原始字符串并按指定的次数重复该字符串。
readString():允许从文件路径直接读取到字符串。
writeString(Path path):将字符串直接写入指定路径处的文件。
indent(int level):缩进字符串的指定量。负值只会影响前导空格。
transform(Function f):将给定的 lambda 应用于字符串。
isBlank():如果字符串为空或字符串仅包含空格(包括制表符),则返回 true。注意与isEmpty() 不同,isEmpty()仅在长度为 0 时返回 true。
lines():将字符串拆分为字符串流,每个字符串包含一行。
strip() : 分别从开头和结尾;
stripLeading()/stripTrailing()仅开始和仅结束删除空格。
repeat(int times):返回一个字符串,该字符串采用原始字符串并按指定的次数重复该字符串。
readString():允许从文件路径直接读取到字符串。
writeString(Path path):将字符串直接写入指定路径处的文件。
indent(int level):缩进字符串的指定量。负值只会影响前导空格。
transform(Function f):将给定的 lambda 应用于字符串。
switch表达式
package cn.dayangshuo;
/**
* @author DAYANG
*/
public class OldSwitch {
public static void main(String[] args) {
oldSwitch();
newSwitch();
}
private static void oldSwitch() {
int size = 3;
String cn = "";
switch (size) {
case 1:
cn = "壹";
break;
case 2:
cn = "贰";
break;
case 3:
cn = "叁";
break;
default:
cn = "未知";
}
System.out.println(cn);
}
//更清爽
private static void newSwitch() {
int size = 3;
String cn = switch (size) {
case 1 -> "壹";
case 2 -> "贰";
case 3, 4 -> "叁";
default -> "未知";
};
System.out.println(cn);
}
}
/**
* @author DAYANG
*/
public class OldSwitch {
public static void main(String[] args) {
oldSwitch();
newSwitch();
}
private static void oldSwitch() {
int size = 3;
String cn = "";
switch (size) {
case 1:
cn = "壹";
break;
case 2:
cn = "贰";
break;
case 3:
cn = "叁";
break;
default:
cn = "未知";
}
System.out.println(cn);
}
//更清爽
private static void newSwitch() {
int size = 3;
String cn = switch (size) {
case 1 -> "壹";
case 2 -> "贰";
case 3, 4 -> "叁";
default -> "未知";
};
System.out.println(cn);
}
}
文本块
Java处理json或者xml文本时,需要多个繁琐的嵌套转义,代码写着费劲,维护更费劲。从Java13开始引入了文本块(Text blocks),上代码:
package cn.dayangshuo;
/**
* @author DAYANG
*/
public class TextBlocks {
public static void main(String[] args) {
//需要转义
String jsonStr = "{\"name\": \"大阳\"}";
//文本块,不需要转义字符
String textBlocks = """
{"name": "大阳"}
""";
System.out.println(jsonStr);
System.out.println(textBlocks);
}
}
package cn.dayangshuo;
/**
* @author DAYANG
*/
public class TextBlocks {
public static void main(String[] args) {
//需要转义
String jsonStr = "{\"name\": \"大阳\"}";
//文本块,不需要转义字符
String textBlocks = """
{"name": "大阳"}
""";
System.out.println(jsonStr);
System.out.println(textBlocks);
}
}
密封类(Sealed classes)
密封的类和接口限制了其他类或接口可以扩展或实现它们
public sealed interface Shape{
final class Planet implements Shape {}
final class Star implements Shape {}
final class Comet implements Shape {}
}
public abstract sealed class Test{
final class A extends Test {}
final class B extends Test {}
final class C extends Test {}
}
public sealed interface Shape{
final class Planet implements Shape {}
final class Star implements Shape {}
final class Comet implements Shape {}
}
public abstract sealed class Test{
final class A extends Test {}
final class B extends Test {}
final class C extends Test {}
}
恢复始终严格的浮点语义
既然是恢复严格的浮点语义,那么说明在某个时间点之前,是始终严格的浮点语义的。其实在 Java SE 1.2 之前,所有的浮点计算都是严格的,但是以当初的情况来看,过于严格的浮点计算在当初流行的 x86 架构和 x87 浮点协议处理器上运行,需要大量的额外的指令开销,所以在 Java SE 1.2 开始,需要手动使用关键字 strictfp(strict float point) 才能启用严格的浮点计算。
但是在 2021 年的今天,硬件早已发生巨变,当初的问题已经不存在了,所以从 Java 17 开始,恢复了始终严格的浮点语义这一特性。
扩展:strictfp 是 Java 中的一个关键字,大多数人可能没有注意过它,它可以用在类、接口或者方法上,被 strictfp 修饰的部分中的 float 和 double 表达式会进行严格浮点计算。
下面是一个示例,其中的 testStrictfp() 被 strictfp 修饰。
package com.wdbyte;
public class Main {
public static void main(String[] args) {
testStrictfp();
}
public strictfp static void testStrictfp() {
float aFloat = 0.6666666666666666666f;
double aDouble = 0.88888888888888888d;
double sum = aFloat + aDouble;
System.out.println("sum: " + sum);
}
}
但是在 2021 年的今天,硬件早已发生巨变,当初的问题已经不存在了,所以从 Java 17 开始,恢复了始终严格的浮点语义这一特性。
扩展:strictfp 是 Java 中的一个关键字,大多数人可能没有注意过它,它可以用在类、接口或者方法上,被 strictfp 修饰的部分中的 float 和 double 表达式会进行严格浮点计算。
下面是一个示例,其中的 testStrictfp() 被 strictfp 修饰。
package com.wdbyte;
public class Main {
public static void main(String[] args) {
testStrictfp();
}
public strictfp static void testStrictfp() {
float aFloat = 0.6666666666666666666f;
double aDouble = 0.88888888888888888d;
double sum = aFloat + aDouble;
System.out.println("sum: " + sum);
}
}
增强的伪随机数生成器
为伪随机数生成器 RPNG(pseudorandom number generator)增加了新的接口类型和实现,让在代码中使用各种 PRNG 算法变得容易许多。
这次增加了 RandomGenerator 接口,为所有的 PRNG 算法提供统一的 API,并且可以获取不同类型的 PRNG 对象流。同时也提供了一个新类 RandomGeneratorFactory 用于构造各种 RandomGenerator 实例,在 RandomGeneratorFactory 中使用 ServiceLoader.provider 来加载各种 PRNG 实现。
下面是一个使用示例:随便选择一个 PRNG 算法生成 5 个 10 以内的随机数。
package com.wdbyte.java17;
import java.util.Date;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
import java.util.stream.Stream;
/**
* @author niulang
*/
public class JEP356 {
public static void main(String[] args) {
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}
}
}
得到输出:
7
3
4
4
6
你也可以遍历出所有的 PRNG 算法。
RandomGeneratorFactory.all().forEach(factory -> {
System.out.println(factory.group() + ":" + factory.name());
});
得到输出:
LXM:L32X64MixRandom
LXM:L128X128MixRandom
LXM:L64X128MixRandom
Legacy:SecureRandom
LXM:L128X1024MixRandom
LXM:L64X128StarStarRandom
Xoshiro:Xoshiro256PlusPlus
LXM:L64X256MixRandom
Legacy:Random
Xoroshiro:Xoroshiro128PlusPlus
LXM:L128X256MixRandom
Legacy:SplittableRandom
LXM:L64X1024MixRandom
可以看到 Legacy:Random 也在其中,新的 API 兼容了老的 Random 方式,所以你也可以使用新的 API 调用 Random 类生成随机数。
// 使用 Random
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("Random");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}
这次增加了 RandomGenerator 接口,为所有的 PRNG 算法提供统一的 API,并且可以获取不同类型的 PRNG 对象流。同时也提供了一个新类 RandomGeneratorFactory 用于构造各种 RandomGenerator 实例,在 RandomGeneratorFactory 中使用 ServiceLoader.provider 来加载各种 PRNG 实现。
下面是一个使用示例:随便选择一个 PRNG 算法生成 5 个 10 以内的随机数。
package com.wdbyte.java17;
import java.util.Date;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
import java.util.stream.Stream;
/**
* @author niulang
*/
public class JEP356 {
public static void main(String[] args) {
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("L128X256MixRandom");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}
}
}
得到输出:
7
3
4
4
6
你也可以遍历出所有的 PRNG 算法。
RandomGeneratorFactory.all().forEach(factory -> {
System.out.println(factory.group() + ":" + factory.name());
});
得到输出:
LXM:L32X64MixRandom
LXM:L128X128MixRandom
LXM:L64X128MixRandom
Legacy:SecureRandom
LXM:L128X1024MixRandom
LXM:L64X128StarStarRandom
Xoshiro:Xoshiro256PlusPlus
LXM:L64X256MixRandom
Legacy:Random
Xoroshiro:Xoroshiro128PlusPlus
LXM:L128X256MixRandom
Legacy:SplittableRandom
LXM:L64X1024MixRandom
可以看到 Legacy:Random 也在其中,新的 API 兼容了老的 Random 方式,所以你也可以使用新的 API 调用 Random 类生成随机数。
// 使用 Random
RandomGeneratorFactory<RandomGenerator> l128X256MixRandom = RandomGeneratorFactory.of("Random");
// 使用时间戳作为随机数种子
RandomGenerator randomGenerator = l128X256MixRandom.create(System.currentTimeMillis());
for (int i = 0; i < 5; i++) {
System.out.println(randomGenerator.nextInt(10));
}
更强的 JDK 内部封装
如 Java 16 的 JEP 396 中描述的一样,为了提高 JDK 的安全性,使 --illegal-access 选项的默认模式从允许更改为拒绝。通过此更改,JDK 的内部包和 API(关键内部 API 除外)将不再默认打开。
但是在 Java 17 中,除了 sun.misc.Unsafe ,使用 --illegal-access 命令也不能打开 JDK 内部的强封装模式了,除了 sun.misc.Unsafe API .
在 Java 17 中使用 --illegal-access 选项将会得到一个命令已经移除的警告。
➜ bin ./java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
➜ bin ./java --illegal-access=warn
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=warn; support was removed in 17.0
但是在 Java 17 中,除了 sun.misc.Unsafe ,使用 --illegal-access 命令也不能打开 JDK 内部的强封装模式了,除了 sun.misc.Unsafe API .
在 Java 17 中使用 --illegal-access 选项将会得到一个命令已经移除的警告。
➜ bin ./java -version
openjdk version "17" 2021-09-14
OpenJDK Runtime Environment (build 17+35-2724)
OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
➜ bin ./java --illegal-access=warn
OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=warn; support was removed in 17.0
移除实验性的 AOT 和 JIT 编译器
在 Java 9 的 JEP 295 中,引入了实验性的提前编译 jaotc 工具,但是这个特性自从引入依赖用处都不太大,而且需要大量的维护工作,所以在 Java 17 中决定删除这个特性。
主要移除了三个 JDK 模块:
jdk.aot - jaotc 工具。
Jdk.internal.vm.compiler - Graal 编译器。
jdk.internal.vm.compiler.management
同时也移除了部分与 AOT 编译相关的 HotSpot 代码:
src/hotspot/share/aot — dumps and loads AOT code
Additional code guarded by #if INCLUDE_AOT
主要移除了三个 JDK 模块:
jdk.aot - jaotc 工具。
Jdk.internal.vm.compiler - Graal 编译器。
jdk.internal.vm.compiler.management
同时也移除了部分与 AOT 编译相关的 HotSpot 代码:
src/hotspot/share/aot — dumps and loads AOT code
Additional code guarded by #if INCLUDE_AOT
弃用 Security Manager
Security Manager 在 JDK 1.0 时就已经引入,但是它一直都不是保护服务端以及客户端 Java 代码的主要手段,为了 Java 的继续发展,决定弃用 Security Manager,在不久的未来进行删除。
@Deprecated(since="17", forRemoval=true)
public class SecurityManager {
// ...
}
@Deprecated(since="17", forRemoval=true)
public class SecurityManager {
// ...
}
指定上下文的反序列化过滤器
Java 中的序列化一直都是非常重要的功能,如果没有序列化功能,Java 可能都不会占据开发语言的主导地位,序列化让远程处理变得容易和透明,同时也促进了 Java EE 的成功。
但是 Java 序列化的问题也很多,它几乎会犯下所有的可以想象的错误,为开发者带来持续的维护工作。但是要说明的是序列化的概念是没有错的,把对象转换为可以在 JVM 之间自由传输,并且可以在另一端重新构建的能力是完全合理的想法,问题在于 Java 中的序列化设计存在风险,以至于爆出过很多和序列化相关的漏洞。
反序列化危险的一个原因是,有时候我们不好验证将要进行反序列化的内容是否存在风险,而传入的数据流可以自由引用对象,很有可能这个数据流就是攻击者精心构造的恶意代码。
所以,JEP 415 允许在反序列化时,通过一个过滤配置,来告知本次反序列化允许或者禁止操作的类,反序列化时碰到被禁止的类,则会反序列化失败。
14.1. 反序列化示例
假设 Dog 类中的 Poc 是恶意构造的类,但是正常反序列化是可以成功的。
package com.wdbyte.java17;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}
class Dog implements Serializable {
private String name;
private Poc poc;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}
class Poc implements Serializable{
}
输出结果:
Dog{name='哈士奇'}
14.2. 反序列化过滤器
在 Java 17 中可以自定义反序列化过滤器,拦截不允许的类。
package com.wdbyte.java17;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// 允许 com.wdbyte.java17.Dog 类,允许 java.base 中的所有类,拒绝其他任何类
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.wdbyte.java17.Dog;java.base/*;!*");
objectInputStream.setObjectInputFilter(filter);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}
class Dog implements Serializable {
private String name;
private Poc poc;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}
class Poc implements Serializable{
}
这时反序列化会得到异常。
Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
at java.base/java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1412)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2053)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1907)
....
但是 Java 序列化的问题也很多,它几乎会犯下所有的可以想象的错误,为开发者带来持续的维护工作。但是要说明的是序列化的概念是没有错的,把对象转换为可以在 JVM 之间自由传输,并且可以在另一端重新构建的能力是完全合理的想法,问题在于 Java 中的序列化设计存在风险,以至于爆出过很多和序列化相关的漏洞。
反序列化危险的一个原因是,有时候我们不好验证将要进行反序列化的内容是否存在风险,而传入的数据流可以自由引用对象,很有可能这个数据流就是攻击者精心构造的恶意代码。
所以,JEP 415 允许在反序列化时,通过一个过滤配置,来告知本次反序列化允许或者禁止操作的类,反序列化时碰到被禁止的类,则会反序列化失败。
14.1. 反序列化示例
假设 Dog 类中的 Poc 是恶意构造的类,但是正常反序列化是可以成功的。
package com.wdbyte.java17;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}
class Dog implements Serializable {
private String name;
private Poc poc;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}
class Poc implements Serializable{
}
输出结果:
Dog{name='哈士奇'}
14.2. 反序列化过滤器
在 Java 17 中可以自定义反序列化过滤器,拦截不允许的类。
package com.wdbyte.java17;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
* @author niulang
*/
public class JEP415 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Dog dog = new Dog("哈士奇");
dog.setPoc(new Poc());
// 序列化 - 对象转字节数组
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);) {
objectOutputStream.writeObject(dog);
}
byte[] bytes = byteArrayOutputStream.toByteArray();
// 反序列化 - 字节数组转对象
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
// 允许 com.wdbyte.java17.Dog 类,允许 java.base 中的所有类,拒绝其他任何类
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"com.wdbyte.java17.Dog;java.base/*;!*");
objectInputStream.setObjectInputFilter(filter);
Object object = objectInputStream.readObject();
System.out.println(object.toString());
}
}
class Dog implements Serializable {
private String name;
private Poc poc;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" + "name='" + name + '\'' + '}';
}
// get...set...
}
class Poc implements Serializable{
}
这时反序列化会得到异常。
Exception in thread "main" java.io.InvalidClassException: filter status: REJECTED
at java.base/java.io.ObjectInputStream.filterCheck(ObjectInputStream.java:1412)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2053)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1907)
....
其他
使用新的 macOS 渲染库
支持 macOS/AArch64 架构
删除已弃用的 Applet API
移除 RMI Activation
0 条评论
下一页