Java 23 :有何新变化?

如今 Java 23 已完成功能特性开发(在撰写本文时处于第二阶段降速期),是时候来了解一下这个新版本为我们开发者带来的所有功能了。
 
首先,发生了一件据我所知在 Java 版本发布中从未发生过的事情:一个预览功能被移除了!在 Java 21 中作为预览功能出现的字符串模板功能已被移除。该功能将进行全面重新设计,因为它引发了很多争议,似乎也没有达到社区的期望。
 
定义预览功能流程的 JEP 12 明确指出,预览功能可以由功能所有者自行决定移除,无需新的 JEP。
 
最终,JEP 所有者必须决定预览功能的命运。如果决定移除预览功能,那么所有者必须在 JBS 中提交一个问题,以便在下次 JDK 功能发布中移除该功能;无需新的 JEP。
这里就是这么做的。
 
有关字符串模板移除的更多信息,请参见 JDK - 8329949 问题单。
 
JEP 455—— 模式、instanceof 和 switch 中的基本类型(预览)

这一预览功能为 instanceof 和 switch 增加了对基本类型的支持,并增强了模式匹配以支持基本类型模式:在 instanceof 中、在 switch 语句中以及在记录解构中。
 
现在,switch 支持所有基本类型。
 
示例:
 
long l =...;
switch (l) {
    case 1L              ->...;
    case 2L              ->...;
    case 10_000_000_000L ->...;
    default ->...;
}
 
现在我们可以对所有基本类型使用 instanceof。
 
来自 JEP 的示例:
if (i instanceof byte) {  // i 的值适合存储在一个字节中
   ... (byte)i...       // 需要传统的强制类型转换
}
但最有趣的是对模式匹配的支持。以下是一些现在可以在何处对基本类型使用模式匹配的示例。
 
来自 JEP 的在 switch 中对基本类型进行模式匹配的示例:
 
switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case 2 -> "error";
    case int i -> "unknown status: " + i;
}
 
还可以通过 when 子句支持守卫:
 
switch (x.getStatus()) {
    case 0 -> "okay";
    case 1 -> "warning";
    case int i when i > 1 && i < 100 -> "client error: " + i;
    case int i when i > 100 -> "server error: " + i;
}
 
 
来自 JEP 的在 instanceof 中对基本类型进行模式匹配的示例:
 
if (i instanceof byte b) {
   ... b...
}
 
来自 JEP 的在解构记录时对基本类型进行模式匹配的示例:
 
// JSON 没有区分整数和双精度数,所以 JSON 数字应该是双精度数。
record JsonNumber(double number) implements JsonValue { }
var number = new JsonNumber(30);
// 以前我们只能通过其确切的组件类型(long)来解构这个记录,
// 现在我们可以解构并匹配不同的基本类型
if (json instanceof JsonObject(int number)) {
    //...
}
 
这种演变需要在模式匹配中实现转换规则,以便一个基本类型与另一个基本类型匹配,如在前面的示例中,30 匹配了一个 int,即使记录组件被定义为 double,目标类型必须被模式测试覆盖。在这里,30 被一个 int 覆盖。未被覆盖的值将被拒绝。
 
有关 JEP 455 的更多信息。
 
JEP 467——Markdown 文档注释

该功能允许你使用 Markdown 而不仅仅是 HTML 和 JavaDoc 标签的混合来编写 JavaDoc 文档注释。
 
编写 HTML 代码并不总是容易的,而且在没有渲染的情况下可读性也不高,JavaDoc 标签有时使用起来很复杂。Markdown 是一种在没有渲染的情况下也具有可读性且易于使用的语言。将其用于 JavaDoc 注释是一个很好的选择。Markdown 支持使用 HTML 标签,提供了极大的灵活性,同时如果需要,仍然支持特定于 JavaDoc 的标签。
 
Markdown 注释以三个斜杠开头:///。
 
以下是来自 JEP 的一个示例:
/**
 * 返回对象的哈希码值。此方法是为了哈希表(如由{@link java.util.HashMap}提供的那些)的利益而支持的。
 * <p>
 * {@code hashCode}的一般约定是:
 * <ul>
 * <li>在 Java 应用程序的一次执行过程中,每当对同一对象多次调用它时,只要在对象的{@code equals}比较中使用的信息没有被修改,{@code hashCode}方法就必须始终返回相同的整数。这个整数在同一应用程序的不同执行之间不需要保持一致。</li>
 * <li>如果根据{@link #equals(Object)}方法两个对象相等,那么对这两个对象中的每一个调用{@code hashCode}方法必须产生相同的整数结果。</li>
 * <li>如果根据{@link #equals(Object)}方法两个对象不相等,那么对这两个对象中的每一个调用{@code hashCode}方法并不要求必须产生不同的整数结果。然而,程序员应该意识到,为不相等的对象产生不同的整数结果可能会提高哈希表的性能。</li>
 * </ul>
 *
 * @implSpec
 * 在合理可行的范围内,由类{@code Object}定义的{@code hashCode}方法为不同的对象返回不同的整数。
 *
 * @return 这个对象的哈希码值。
 * @see     java.lang.Object#equals(java.lang.Object)
 * @see     java.lang.System#identityHashCode
 */

用 Markdown 可以这样写:

/// 返回对象的哈希码值。此方法是为了哈希表(如由[java.util.HashMap]提供的那些)的利益而支持的。
///
/// `hashCode`的一般约定是:
///
///   - 在 Java 应用程序的一次执行过程中,每当对同一对象多次调用它时,只要在对象的`equals`比较中使用的信息没有被修改,`hashCode`方法就必须始终返回相同的整数。这个整数在同一应用程序的不同执行之间不需要保持一致。
///   - 如果根据[equals][#equals(Object)]方法两个对象相等,那么对这两个对象中的每一个调用`hashCode`方法必须产生相同的整数结果。
///   - 如果根据[equals][#equals(Object)]方法两个对象不相等,那么对这两个对象中的每一个调用`hashCode`方法并不要求必须产生不同的整数结果。然而,程序员应该意识到,为不相等的对象产生不同的整数结果可能会提高哈希表的性能。
///
/// @implSpec
/// 在合理可行的范围内,由类`Object`定义的`hashCode`方法为不同的对象返回不同的整数。
///
/// @return 这个对象的哈希码值。
/// @see     java.lang.Object#equals(java.lang.Object)
/// @see     java.lang.System#identityHashCode
有关 JEP 467 的更多信息。
 
JEP 471—— 弃用 sun.misc.Unsafe 中的内存访问方法以准备移除

正如其名称所示,Unsafe 是一个内部且不受支持的 JDK 类,调用它是不安全的。由于历史原因,许多底层框架使用 Unsafe 进行更快的内存访问。由于有了 VarHandle API(JEP 193,自 Java 9 起)和外部函数与内存 API(JEP 454,自 Java 22 起)功能,现在有了 Unsafe 内存访问方法的替代品,它们同样强大,但更安全且更受支持。总共有超过 Unsafe 的 87 个方法中的 79 个受到影响,这使我们更接近可以弃用并删除整个类的地步!
 
弃用这些方法清楚地表明是时候使用这些替代品了!然而,我们大多数人不应该看到这些变化,因为除了在框架或库中,Unsafe 很少在其他地方被使用。
 
这些方法将逐步被降级和弃用:
 
  • 阶段 1:在 Java 23(这个 JEP)中弃用。
  • 阶段 2:在 Java 24 或 25 中如果在运行时使用则记录警告。
  • 阶段 3:在 Java 26 或更高版本中默认抛出异常(行为可通过命令行选项修改)。
  • 阶段 4 和 5:在 Java 26 之后移除这些方法(先移除堆上内存访问方法,然后移除堆外内存访问方法)。
 
有关 JEP 471 的更多信息。
 
JEP 474——ZGC:默认情况下的分代模式

ZGC 是一种垃圾收集器,旨在支持非常大的堆(数 TB)且暂停时间非常短(毫秒级)。
 
通过 JEP 439 在 Java 21 中添加分代堆使其能够在消耗更少资源的同时支持不同的工作负载。
 
现在分代模式是默认模式。
 
有关 JEP 474 的更多信息。
 
JEP 476—— 模块导入声明(预览)

在 Java 中,你可以导入:
  • 用语句import java.util.*;导入一个包中的所有类。
  • 用语句import java.util.Map;导入单个类。
  • 用语句import static org.junit.jupiter.api.Assertions.*;导入一个类的所有静态方法和变量。
  • 用语句import static org.junit.jupiter.api.Assertions.assertTrue;导入单个静态方法或变量。

然而,以前不可能用一个语句导入一个模块的所有类。现在可以用import module java.base;语句来实现,这个语句可以在一个语句中导入从java.base模块导出的所有包中的所有类,以及java.base模块间接需要的模块中的类。
 
有关 JEP 476 的更多信息。
 
退出预览的功能

在 Java 23 中,没有以前处于预览(或孵化模块)的功能退出预览或孵化。
 
当然,除了我在介绍中提到的字符串模板功能,它已从预览中移除且不知去向。
 
仍处于预览的功能

以下功能仍处于预览(或在孵化模块中)。
 
  • JEP 466—— 类文件 API:第二次预览,用于解析、生成和转换 Java 类文件的标准 API。根据用户反馈进行了改进。在 Java 23 中,JDK 继续向使用这个新 API 迁移。
  • JEP 469—— 向量 API:第八次孵化,用于表示向量计算的 API,在运行时编译为支持的 CPU 架构的向量指令。没有变化:JEP 同意向量 API 将继续处于孵化状态,直到 Valhalla 项目的功能作为预览可用。这是预期的,因为向量 API 将能够利用 Valhalla 项目预期的性能和内存表示改进。
  • JEP 473—— 流收集器:第二次预览,通过支持自定义中间操作增强了 Stream API。没有变化。
  • JEP 477—— 隐式声明的类和实例主方法:第三次预览,通过允许在隐式类(无需声明)和void main()实例方法中定义简单程序,简化了简单程序的编写。有两个变化:隐式类自动导入新的java.io.IO类的三个静态方法print(Object)println(Object)readln(Object),并且它们根据需要自动导入java.base模块中的包中的类。
  • JEP 480—— 结构化并发:第三次预览,一个新的 API,通过允许将多个并发任务视为单个处理单元,简化了多线程代码的编写。没有变化。
  • JEP 481—— 作用域值:第三次预览,允许在线程内部和线程之间共享不可变数据。有一个小变化。
  • JEP 482—— 灵活的构造函数体:第二次预览,一个允许在父构造函数之前调用指令的功能,只要它们不访问当前正在创建的实例。构造函数现在可以在显式调用构造函数之前初始化同一类的字段。
 
杂项

对 JDK 进行了各种添加:
 
  • 随着 JEP 477—— 隐式声明的类和实例主方法的推出,除了新的IO类之外,相同的三个方法也被添加到了Console类中:print(Object)println(Object)readln(Object)
  • Console类增加了三个使用带有区域设置格式化的字符串的新方法:format(Locale, String, Object)printf(Locale, String, Object)readLine(Locale, String, Object)
  • Console.readPassword(Locale, String, Object):与Console.readPassword(String, Object)相同,但接受一个区域设置作为参数用于字符串本地化。
  • Inet4Address.ofPosixLiteral(String):根据以 POSIX 兼容形式inet_addr提供的 IPv4 地址的文本表示创建一个Inet4Address
  • java.text.NumberFormat及其后代增加了setStrict(boolean)isScript()方法,可用于更改格式化模式;默认模式是严格模式。
  • Instant.until(Instant):计算到另一个Instant的持续时间。
 
以下方法已被移除,它们已被标记为要删除,并在以前的版本中已被降级为抛出异常:
 
  • Thread.resume()Thread.suspend()
  • ThreadGroup.resume()ThreadGroup.stop()ThreadGroup.suspend()
 
所有新的 JDK 23 API 可以在《Java 版本年鉴 ——Java 23 中的新 API》中找到。
 
内部变化、性能和安全性

并行垃圾收集器(Parallel GC)对其完全垃圾收集算法进行了重新实现,以使用更经典的并行标记 - 清除 - 压缩算法。这与 G1 垃圾收集器使用的算法相同,在某些特定情况下优化了性能,在使用并行垃圾收集器时减少了 1.5% 的堆使用量。在垃圾收集器方面还进行了其他更改,可以在 Thomas Schatzl 的这篇文章中找到:《JDK 23 G1/Parallel/Serial GC 更改》
 
我还没有注意到其他值得注意的变化,但如果我发现更多变化,我会更新这篇文章。
 
JFR 事件

没有新的 Java 飞行记录器(JFR)事件。
 
你可以在页面 “JFR 事件” 上找到此版本的 Java 支持的所有 JFR 事件。
 
结论

这个新版本的 Java 在新功能方面相当稀少,目前正在开发的功能中很少有退出预览的。
 
尽管在模式匹配中对基本类型的支持以及在 JavaDoc 中对 Markdown 的支持是非常有趣的改进,但字符串模板的消失且没有替代品意味着对这个期待已久的功能的支持还遥遥无期。
 
要查找 Java 23 中的所有更改,请参考发行说明。若你想提升Java技能,可关注我们的Java培训课程。