Java 26 正式发布:为未来技术演进奠定坚实基础
- HotSpot
- 核心库 重新预览特性
- JEP 524:加密对象的 PEM 编码(第二次预览)
- JEP 525:结构化并发(第六次预览)
- JEP 526:惰性常量(第二次预览)
- JEP 529:向量 API(第十一次孵化)
- JEP 530:模式、instanceof 和 switch 中的原生类型(第四次预览) 弃用项
- JEP 500:准备将 final 设为真正不可变
- JEP 504:移除 Applet API 结语
Java 26 现已发布!六个月前,我们迎来了 Java 25,这意味着又到了更新 Java 特性的时候。与之前的版本相比,本次发布的特性集规模较小,但这只意味着一件事:该版本的重点是为即将到来的重大更新奠定坚实的基础!我希望 Valhalla 项目的首批 JEP 能在今年晚些时候发布。Java 26 中的一些改动增强了这种期待,因为它们感觉像是为 Valhalla 特性所做的必要准备步骤(特别是 JEP 500 和 529)。
抛开未来计划不谈,本文将重点介绍此版本中添加的所有内容,并对每个特性进行简要说明。在适用的情况下,文中会强调与 Java 25 的差异,并提供一些典型用例,以便您在阅读后能够立即上手使用这些特性。
JEP 概览
首先,让我们看看 Java 26 中包含的 JEP 概览。下表列出了它们的预览状态、所属项目、添加的特性类型以及自 Java 25 以来的变动。
新特性
让我们从为 Java 26 添加全新特性的 JEP 开始。
HotSpot
Java 26 在 HotSpot 中引入了两个新特性:
- JEP 516:任意 GC 的提前对象缓存
- JEP 522:G1 GC:通过减少同步提高吞吐量
HotSpot JVM 是由 Oracle 开发的运行时引擎。它将 Java 字节码转换为宿主操作系统处理器架构的机器码。
JEP 516:任意 GC 的提前对象缓存
对于需要快速响应时间的应用(如 Web 服务器或实时系统),一个重要的指标是尾延迟(即处理请求所需的时间)。
延迟可能由垃圾回收暂停引起,也可能由发送到尚未预热的新 JVM 实例的请求引起。
前者可以通过使用低延迟垃圾回收器(如 ZGC)来缓解,而后者可以通过使用提前缓存(AOT cache)来缓解,从而使 JVM 实例启动更快。
Java 24 引入了这种提前缓存,在经过初始“训练运行”后,将类读取、解析、加载和链接到内存中。随后,它可以在应用的后续运行中被重用,以缩短启动时间。
然而,当时缓存的使用受到一定限制,因为缓存的 Java 对象以特定于 GC 的格式存储,导致其与 ZGC 等其他垃圾回收器不兼容。JEP 516 通过以 GC 无关的格式缓存 Java 对象,将提前缓存的支持扩展到了 ZGC(以及任何其他垃圾回收器)。
新缓存格式为何与 GC 无关?
每个垃圾回收器都有其在内存中布局对象的策略,这意味着缓存对象的内存地址在不同的垃圾回收器之间是无效的。
为了解决这个问题,JEP 516 通过用逻辑索引替换内存地址来改变缓存格式。当缓存被加载时,这些逻辑索引通过“流式传输”到内存中,并在此过程中将缓存的对象实例化,从而转换回内存地址。
用法
如果在训练过程中使用了 ZGC 或 -XX:-UseCompressedOops 命令行选项,或者堆内存大于 32GB,JVM 将自动以可流式传输、与 GC 无关的格式缓存对象。相反,如果在训练过程中使用了 -XX:+UseCompressedOops 命令行选项(注意加号),它将以旧的、特定于 GC 的格式缓存对象。这表明训练环境的堆内存小于 32GB 且未使用 ZGC。
如果您想强制使用新的与 GC 无关的缓存格式,无论训练环境如何,都可以通过指定 -XX:+AOTStreamableObjects 命令行选项来实现。
为什么不简单地创建 ZGC 专用缓存?
我们没有采用创建 ZGC 专用缓存的替代方案,因为它需要为每个垃圾回收器维护单独的缓存。此外,ZGC 专用缓存的唯一好处是在单核机器上性能稍好一些。这种好处在实践中可以忽略不计,因为高并发的 ZGC 设计初衷是为多核机器而非单核机器提供高性能,且这种好处不足以抵消维护多个缓存的成本。
更多信息
有关此特性的更多信息,请阅读 JEP 516。
JEP 522:G1 GC:通过减少同步提高吞吐量
G1 GC 自 Java 9 起成为 Java 的默认垃圾回收器。它旨在为具有大堆内存的应用提供高性能和低停顿时间,旨在平衡延迟和吞吐量。为了实现这种平衡,G1 与应用并发执行,使得应用线程与 GC 线程共享 CPU。这种情况需要线程同步,但不幸的是,这会降低吞吐量并增加延迟。
JEP 522 提议通过减少应用线程和 GC 线程之间所需的同步量来提高吞吐量和延迟。
为什么目前需要同步?
当 G1 回收内存时,堆中的存活对象会被复制到新的内存区域,从而释放它们留下的空间。指向这些对象的引用必须更新以指向其新位置。为了避免扫描整个堆以查找这些对象的现有引用,G1 维护了一个称为“卡表(card table)”的数据结构,每当对象引用存储在字段中时,它就会被更新。这些更新由称为“写屏障(write barriers)”的代码片段执行,G1 与即时(JIT)编译器协作将这些代码注入到应用中。
扫描卡表是一个高效的操作,通常可以在 GC 暂停的时间窗口内完成。然而,在对象分配非常频繁的环境中,卡表可能会变得太大而无法在 G1 的暂停时间目标内完成扫描。为了避免这种情况,G1 通过单独的优化器线程在后台优化卡表。这种方法只有在以线程安全的方式更新卡表时才有效,目前是通过将优化器线程与应用线程同步来实现的。可以说,这导致了更复杂、更慢的写屏障代码。
迈向第二个卡表
JEP 522 提议引入一个“第二个卡表”,以确保优化器线程和应用线程不再互相干扰。应用线程中的写屏障将在没有任何同步的情况下更新第一个卡表,而优化器线程将更新第二个初始为空的卡表。
当 G1 判断在暂停期间扫描当前卡表可能会违反停顿时间目标时,它会原子地交换这两个卡表。应用线程随后继续写入现在为空的表(即原来的“第二个”表),而专门的优化器线程则处理之前填满的表(即原来的“第一个”表),无需任何额外的同步。G1 会根据需要重复此交换过程,使活动卡表上的工作保持在预期限制内。
这种方法减少了应用线程和优化器线程之间所需的同步量。在频繁修改对象引用字段的应用中,预计吞吐量可提升 5-15%。此外,由于写屏障代码变得简单得多,即使在不频繁修改对象引用字段的应用中,在 x64 架构上也观察到了高达 5% 的吞吐量提升。
这两个卡表大小相同,每个都占用额外的原生内存。它们总共占用 Java 堆约 0.2% 的空间,这意味着每 GB 堆空间约占用 2MB 原生内存。这种适度的开销换取了可观的性能提升——特别是考虑到在 Java 20 之前,G1 所需的内存是第二个卡表现在所需内存的八倍以上。
更多信息
有关此特性的更多信息,请阅读 JEP 522。
核心库
Java 26 引入了一个属于核心库的单一新特性:
- JEP 517:HTTP Client API 的 HTTP/3 支持
JEP 517:HTTP Client API 的 HTTP/3 支持
自 Java 11 起,Java 平台内就提供了一个现代的 HTTP 客户端 API。它同时支持 HTTP/1.1 和 HTTP/2,并且在设计时就考虑到了对未来版本的潜在支持。在其当前形式下,该 API 默认假设使用 HTTP/2,但如果目标服务器不支持较新的 HTTP 版本,它可以回退到 HTTP/1.1。
下面的代码示例展示了该 API 的易用性和协议无关性:
正如您所见,我们在代码示例中没有指定任何 HTTP 版本——API 默认假设使用 HTTP/2。
HTTP/3:HTTP 的下一个版本
HTTP/3 由 IETF 于 2022 年标准化,它在 TCP 之上使用 QUIC 传输层协议。使用 HTTP/3 协议的应用可以从多路复用、更快的握手、避免网络拥塞问题和更可靠的传输中受益。大多数 Web 浏览器已经支持 HTTP/3,并且约三分之一的网站目前受益于其特性。因此,现在是在 HTTP 客户端 API 中开始支持它的好时机,这正是 JEP 517 所提议的。
在 Java 代码中使用 HTTP/3
在 Java 26 中,HTTP 客户端 API 要求您通过配置 HttpClient 或 HttpRequest 的实例并指定 HTTP_3 版本来选择使用 HTTP/3。例如:
一旦选择了 HTTP/3(无论是在请求本身还是在客户端中),您就可以像通常那样传输请求。如果目标服务器缺乏 HTTP/3 支持,请求将自动且透明地回退到 HTTP/2,或者在必要时回退到 HTTP/1.1。
协议版本协商
HTTP 客户端 API 无法确定目标服务器是否支持 HTTP/3。此外,现有的 HTTP/1.1 和 HTTP/2 连接无法升级到 HTTP/3,因为 HTTP/1.1 和 HTTP/2 建立在 TCP 之上,而 HTTP/3 的 QUIC 基于 UDP 数据报。因此,API 需要一种协商协议版本的方法——为了做到这一点,它配备了四种不同的方法:
- 优先尝试 HTTP/3,超时后回退 —— 以 HTTP/3 发起请求;如果无法在合理的超时时间内建立连接,则自动降级到 HTTP/2 或 HTTP/1.1。(匹配首选版本设置为
HTTP_3的HttpRequest) - HTTP/3 与旧协议竞速 —— 同时打开 HTTP/3 连接和 HTTP/2 或 HTTP/1.1 连接,并使用先成功的连接。(当
HttpClient首选HTTP_3但HttpRequest未指定首选版本时发生) - 以 HTTP/2 或 1.1 开始,并在发现后切换 —— 通过 HTTP/2 或 HTTP/1.1 发送初始请求。如果服务器的响应表明 HTTP/3 可用,则为后续所有请求切换到 HTTP/3。(通过为
H3_DISCOVERY选项设置Http3DiscoveryMode.ALT_SVC触发,且客户端或请求中至少有一个首选HTTP_3) - 强制仅使用 HTTP/3 —— 每个请求都仅通过 HTTP/3 发送;如果服务器无法以 HTTP/3 回复,则将其视为失败,不回退到早期协议。(通过为
H3_DISCOVERY选项设置Http3DiscoveryMode.HTTP_3_URI_ONLY启用,且客户端或请求中至少有一个首选HTTP_3)
这四种方法各有优缺点:
- 选项 1 在回退前会产生超时延迟。
- 选项 2 可能会因建立从未被重用的 HTTP/3 连接而浪费资源。
- 选项 3 需要在实现任何 HTTP/3 好处之前进行初始的 HTTP/2 或 HTTP/1.1 往返。
- 选项 4 仅在您已经知道目标服务器支持 HTTP/3 时才有效。
HTTP/3 的部署范围不如其前辈广泛,这就是为什么没有单一方法可以在所有情况下工作的原因。这也是 HTTP/3 目前无法成为默认协议版本的主要原因,不过当 HTTP/3 被更广泛采用时,这种情况可能会在未来发生改变。
更多信息
有关此特性的更多信息,请阅读 JEP 517。
重新预览特性
现在让我们看看一些您可能已经熟悉的特性,因为它们是在 Java 的先前版本中引入的。它们在 Java 26 中进行了重新预览,在大多数情况下与 Java 25 相比只有微小的变动。### JEP 524:加密对象的 PEM 编码(第二次预览)
在 Java 环境中,公钥、私钥和证书等加密对象可以轻松创建和分发。但在 Java 世界之外,事实上的标准是 PEM(私密增强邮件) 格式。让我们看一个 PEM 编码加密对象的示例:
目前 Java 平台不包含用于解码和编码 PEM 格式文本的易用 API,这意味着解码 PEM 编码的密钥是一项繁琐的工作,需要对源 PEM 文本进行细致的解析。为了进一步说明这一点,目前加密和解密私钥需要十几行代码。
为了解决这个问题,JEP 524 引入了一个可以将对象编码为 PEM 格式的 API。它实际上充当了 Base64 与加密对象之间的桥梁。它在 java.security 包中涉及一个新接口和三个新类:
DEREncodable
: 一个密封接口,用于组合所有支持将其实例与 DER(可辨别编码规则) 格式的字节数组进行相互转换的加密对象。
PEMEncoder
: 一个用于声明将 DEREncodable 对象编码为 PEM 文本的方法的类。
PEMDecoder
: 一个用于声明将 PEM 文本解码为 DEREncodable 对象的方法的类。
PEM
: 一个实现 DEREncodable 的记录(record),可以保存任何类型的 PEM 数据。它允许你对目前尚无 Java 表示形式的 PEM 文本进行编码和解码。
典型用法
以下代码示例展示了该 API 的典型用法:
预览警告
请注意,此 JEP 处于预览阶段,因此你需要向命令行添加 --enable-preview 标志才能试用此功能。
与 Java 25 的区别
与 Java 25 相比,API 做出了一些小改动:
PEMRecord更名为PEM,并包含一个返回解码后 Base64 内容的decode()方法;PEMEncoder和PEMDecoder类现在支持KeyPair和PKCS8EncodedKeySpec对象的加密和解密;- 最后,
EncryptedPrivateKeyInfo类进行了一些更改:encryptKey方法现更名为encrypt,并且它们现在接受DEREncodable对象而不是PrivateKey对象,从而支持对KeyPair和PKCS8EncodedKeySpec对象进行加密。- 包含
getKeyPair方法,用于解密包含PublicKey的 PKCS#8 编码文本。 getKey方法抛出的异常现在与相邻的getKeySpec方法抛出的异常保持一致。
更多信息
有关此功能的更多信息,请参阅 JEP 524。
JEP 525:结构化并发(第六次预览)
Java 的并发处理方式一直是非结构化的,这意味着任务之间独立运行。没有层级、作用域或其他结构,导致错误或取消意图难以传达。为了说明这一点,我们来看一个餐厅场景的代码示例:
这些代码示例摘自我的演讲 "Java 的并发之旅继续!探索结构化并发和作用域值"。
请注意,Waiter 类中的 announceCourse(..) 方法有时会因 OutOfStockException 而失败,因为课程所需的某种配料可能缺货。这会导致一些问题:
- 如果
zoe.announceCourse(CourseType.MAIN)执行时间很长,但在此期间grover.announceCourse(CourseType.STARTER)失败了,announceMenu(..)方法会不必要地通过阻塞main.get()来等待主菜公告,而不是取消它(这才是明智的做法)。 - 如果
zoe.announceCourse(CourseType.MAIN)发生异常,main.get()会抛出该异常,但grover.announceCourse(CourseType.STARTER)将继续在自己的线程中运行,从而导致线程泄漏。 - 如果执行
announceMenu(..)的线程被中断,中断不会传播到子任务:所有运行announceCourse(..)调用的线程都会泄漏,即使在announceMenu()失败后仍会继续运行。
归根结底,这里的问题在于我们的程序在逻辑上构成了任务-子任务关系,但这些关系仅存在于开发者的脑海中。我们可能都更喜欢读起来像顺序故事的结构化代码,但这个示例显然不符合该标准。
相比之下,单线程代码的执行总是强制执行任务和子任务的层级,如我们餐厅示例的单线程版本所示:
在这里,我们不会遇到之前遇到的任何问题。
我们的服务员 Elmo 会以正确的顺序宣布课程,如果某个子任务失败,其余任务甚至不会开始。
而且由于所有工作都在同一个线程中运行,因此不存在线程泄漏的风险。
从这些例子中可以明显看出,如果能够像单线程代码那样强制执行任务和子任务的层级,并发编程将会变得更加容易和直观。
引入结构化并发
在结构化并发方法中,线程具有清晰的层级、自己的作用域以及明确的进入和退出点。结构化并发像函数调用一样按层级排列线程,形成一棵具有父子关系的树。执行作用域会持续到所有子线程完成,从而与代码结构相匹配。
失败时关闭
现在让我们看看餐厅示例的结构化并发版本:
作用域的目的是将线程保持在一起。在 1 处,我们等待 (join) 直到所有线程完成工作。如果其中一个线程被中断,则会抛出 InterruptedException。如果其中一个生成的线程发生异常,这里也会抛出 RuntimeException。一旦到达 2,我们可以确定一切顺利,并可以获取和处理结果。
实际上,与我们之前的代码相比,主要区别在于我们在新的 scope 中创建线程 (fork)。现在我们可以确定这三个线程的生命周期仅限于此作用域内,这与 try-with-resources 语句的主体相吻合。
此外,我们获得了短路行为。当其中一个 announceCourse(..) 子任务失败时,如果其他任务尚未完成,它们将被取消。我们还获得了取消传播。当运行 announceMenu() 的线程在调用 scope.join() 之前或期间被中断时,当线程退出作用域时,所有子任务都会自动取消。
成功时关闭
提供作用域的工厂方法 (StructuredTaskScope.open()) 默认实现了一种“失败时关闭”策略,如果其中一个任务失败,它会取消作用域中的所有剩余任务。此外还提供了一种“成功时关闭”策略:如果其中一个任务成功,它会取消作用域中的所有剩余任务。当已经获得成功结果时,可以使用它来避免执行不必要的工作。
我们可以通过调用接受 Joiner 作为参数的 StructuredTaskScope.open() 方法重载来使用“成功时关闭”策略。让我们看看它的样子:
在此示例中,服务员负责根据客人的偏好和吧台的库存获取有效的 DrinkOrder 对象。
在 Waiter.getDrinkOrder(Guest guest, DrinkCategory... categories) 方法中,服务员开始列出传递给该方法的饮料类别中所有可用的饮品。
一旦客人听到他们喜欢的饮品,他们就会做出回应,服务员会创建一个饮料订单。发生这种情况时,getDrinkOrder(..) 方法返回一个 DrinkOrder 对象,作用域将关闭。
这意味着任何未完成的子任务(例如 Elmo 仍在列出不同种类的茶的任务)都将被取消。
1 处的 join() 方法将返回一个有效的 DrinkOrder 对象,如果其中一个子任务失败,则会抛出 RuntimeException。
更多关闭策略
到目前为止,我们已经看到了两种关闭策略的示例,但通过 StructuredTaskScope.Joiner 接口中的静态工厂方法,还提供了四种现成的策略。例如,Joiner.allSuccessfulOrThrow() 将保持作用域存活,直到所有子任务成功完成,如果任何子任务失败,则将其取消。而 Joiner.awaitAll() 将等待所有子任务完成,无论它们是否成功。也可以通过实现 Joiner 接口来创建自己的关闭策略。这将允许你完全控制何时关闭作用域以及收集哪些结果。
与 Java 25 的区别
与 Java 25 相比,API 做出了一些小改动:
Joiner接口中的一个新方法onTimeout()允许该接口的实现类在超时过期时返回结果。Joiner::allSuccessfulOrThrow()现在返回结果列表,而不是子任务流。Joiner::anySuccessfulResultOrThrow()更名为稍显简洁的anySuccessfulOrThrow()。- 过去接受
Joiner和用于修改默认配置的Function的静态open方法,现在接受Joiner和UnaryOperator。
预览警告
请注意,此 JEP 处于预览阶段,因此你需要向命令行添加 --enable-preview 标志才能试用此功能。
更多信息
如果你想了解更多信息,请参阅 JEP 525,其中详细介绍了此功能的当前状态。### JEP 526:延迟常量(第二次预览)
不可变对象比可变对象简单得多,因为它们始终处于单一状态,并且可以在多个线程之间自由共享。
目前,在 Java 中实现不可变性的主要工具是 final 字段。但它们有两个缺点,限制了其在许多实际应用中的潜力:
- 必须立即(eagerly)初始化;
- 多个
final字段的初始化顺序无法更改,因为它由字段声明的文本顺序决定。
考虑以下吉他商店领域的代码示例,了解不可变性的使用场景:
每当创建 OrderController 实例时,logger 字段都会被立即初始化,这可能会导致 OrderController 的创建变慢。在应用程序中,可能不止一处会这样立即初始化 logger 字段:
所有这些初始化工作都会导致应用程序启动变慢,最糟糕的是:这可能根本没必要!如果用户只是在浏览吉他商店,并没有订购新吉他的打算,那么 OrderController 根本不会被调用,我们却白白初始化了 logger 字段。
为更灵活的初始化牺牲不可变性
我们目前唯一的替代方案是采用基于可变性的方法,将复杂对象的初始化尽可能推迟:
这改善了应用程序的启动速度,但自身也存在一些缺点:
- 对
logger字段的所有访问都必须通过getLogger方法,如果不遵守此规范的代码,则有遇到NullPointerException的风险; - 在多线程环境中,并发调用
submitOrder方法可能会创建多个 logger 对象; - 对已初始化
logger字段的常量折叠访问不再可行,因为 JVM 无法确信其内容在初始更新后永远不会改变。
我们需要的是一种两全其美的解决方案:
- 保证字段在使用时已被初始化;
- 值最多计算一次;
- 在并发环境下是安全的。
换句话说,我们需要延迟不可变性(defer immutability),并希望 Java 运行时对此提供一流的支持。
延迟常量(Lazy Constants)
JEP 526 以延迟常量的形式引入了这种一流的支持。延迟常量是一个 LazyConstant 类型的对象,持有一个单一的数据值。它必须在内容首次被获取前完成初始化,此后变为不可变。
让我们重写 OrderController 类,为其 logger 使用延迟常量:
最初,延迟常量处于未初始化状态。当通过 get() 方法首次访问时,它会通过调用传递给 of() 工厂方法的 lambda 表达式来进行初始化。如果延迟常量已经初始化,那么 get 方法将直接返回其内容。因此,get 方法保证了所提供的 lambda 表达式只会被评估一次(即使在并发调用时也是如此)。
观察延迟常量的特性,可以看出它们填补了 final 和非 final 字段之间的空白:
延迟常量的用法当然不仅限于 logger——我们也可以使用延迟常量来存储 OrderController 组件本身及其相关组件:
应用程序的启动时间得到了改善,因为它不再预先初始化 OrderController 等组件。相反,它通过相应延迟常量的 get 方法按需初始化每个组件。此外,每个组件也会以同样的方式按需初始化其子组件(如 logger)。在底层,JVM 会将声明为 final 的延迟常量内容视为常量,从而允许进行常量折叠优化。
延迟列表(Lazy Lists)
如果你需要跟踪多个延迟常量(例如维护一个对象池)怎么办?我们可以通过延迟列表来实现:
在这里,ORDERS 不再是一个延迟常量,而是一个延迟列表,其中每个元素都存储在一个延迟常量中。客户端调用 ORDERS.get(...) 并传入索引,首次调用时会触发 lambda 函数(忽略索引并调用 OrderController() 构造函数)。后续使用相同索引调用 ORDERS.get(...) 将立即返回元素内容。
延迟映射(Lazy Maps)
或者,我们可以使用延迟映射解决该问题。其键在构造时已知,值存储在延迟常量中,并由构造时提供的计算函数按需初始化:
在此示例中,OrderController 实例与线程名称(本例中为 "Customers", "Internal", "Testing")关联,而不是与从线程 ID 计算出的整数索引关联。延迟映射比延迟列表提供了更具表现力的访问习惯,除此之外具有相同的优点。
与 Java 25 有何不同?
曾经被称为“稳定值”(stable values)的功能被重命名为“延迟常量”,以更好地体现其预期的宏观使用场景。其他变化包括:
- 移除了底层方法
orElseSet、setOrThrow和trySet,仅保留接收值计算函数的工厂方法; - 将延迟列表 (
StableValue.list) 和映射 (StableValue.map) 的工厂方法分别移入List和Map接口中,以增强可发现性; - 将“稳定供应器”(stable suppliers)背后的理念整合到新的
LazyConstant.get()方法中; - 移除了
function和intFunction工厂方法,以进一步简化 API; - 禁止使用
null作为计算值,以提高性能并使延迟常量与不可修改集合、作用域值等构造更好地对齐。
更多信息
如果您想了解更多信息,JEP 526 提供了有关此功能的当前状态的更多详细信息。
JEP 529:向量 API(第十一次孵化)
向量 API 使得表达向量计算成为可能,这些计算可以在运行时可靠地编译为最优的向量指令。这意味着在支持的 CPU 架构(x64 和 AArch64)上,这些计算的性能将显著优于等效的标量计算。
什么是向量计算?
向量计算是对一个或多个任意长度的一维矩阵进行的数学运算。可以将向量视为具有动态长度的数组。此外,向量中的元素可以像数组一样通过索引在常量时间内访问。
过去,Java 程序员只能在汇编代码级别编写此类计算。但现在现代 CPU 支持先进的 SIMD(单指令多数据)特性,利用 SIMD 指令和并行操作的多通道带来的性能增益变得愈发重要。向量 API 将这种可能性带给了 Java 程序员。
代码示例
以下是(摘自 JEP 的)代码示例,比较了数组元素上的简单标量计算与使用向量 API 的等效计算:
从 Java 开发者的角度来看,这只是表达标量计算的另一种方式。虽然看起来可能更冗长,但另一方面,它可以带来显著的性能提升。
典型用例
向量 API 提供了一种在 Java 中编写高性能复杂向量算法的方法,例如向量化的 hashCode 实现或专门的数组比较。许多领域都能从中受益,包括机器学习、线性代数、加密、文本处理、金融以及 JDK 本身的代码。
与 Java 25 有何不同?
与 Java 25 中该功能的第十次孵化版本相比,没有进行任何更改或添加。向量 API 将继续孵化,直到 Project Valhalla 的必要功能作为预览特性可用为止。届时,向量 API 将进行适配以使用这些新特性,并从孵化阶段提升为预览阶段。
更多信息
有关此功能的更多信息,请阅读 JEP 529。### JEP 530:模式、instanceof 和 switch 中的基本类型(第四次预览)
自 Java 23 以来,模式匹配已在所有模式上下文以及 instanceof 和 switch 结构中支持基本类型。该特性已连续三次处于预览状态,在 Java 26 中将进行第四次预览。在重点介绍第四次预览的变化之前,我们先回顾一下它与 Java 22 的区别。
Switch 的模式匹配
Java 22 版本中的 switch 模式匹配 不支持指定基本类型的类型模式。Java 23 增加了对 switch 中基本类型模式的支持,允许将以下代码示例:
改写为:
这还允许使用“守卫条件”(guards)来检查匹配的值,例如:
Record 模式
Record 模式 对基本类型的支持曾经非常有限。
回想一下,Record 模式会将 Record 分解为其各个组件,但当其中一个组件是基本类型时,Record 模式必须精确匹配其类型。为了说明这一点,请看以下代码示例:
换句话说,Java 编译器会将提供的 int 扩展为 double,但不会将其自动缩窄回 int。这种限制存在的原因是缩窄转换可能导致数据丢失:运行时 double 的值可能超过 int 的范围,或者具有超过 int 所能容纳的精度。然而,模式匹配的一个重要优势在于它可以通过“不匹配”来自动拒绝无效值。如果 Tuner 的 double 组件太大或精度过高而无法安全转换为 int,那么 instanceof Tuner(int p) 将直接返回 false,从而允许程序在另一个代码分支中处理该情况。
这与模式匹配目前对引用类型模式的行为类似。例如:
这里可以使用 instanceof 来尝试将 SingleEffect 与 Delay 或 Reverb 组件进行匹配;如果模式匹配成功,它会自动进行窄化。
总之,该 JEP 旨在使基本类型模式像引用类型模式一样顺畅工作,即使对应的 Record 组件是除 int 之外的其他数值基本类型,也允许使用 Tuner(int p)。
instanceof 的模式匹配
Java 22 版本中的 instanceof 模式匹配 不支持基本类型模式,但这一功能与 instanceof 的目的完全吻合:测试一个值是否可以安全地转换为给定类型。为了安全地转换基本类型,Java 开发者过去不得不处理有损转换和范围检查,以防止信息丢失:
该 JEP 提议用对基本类型进行操作的简单 instanceof 检查来替代这些结构。让我们重写代码示例以利用此功能:
模式 roomSize instanceof byte r 仅在 roomSize 能放入 byte 时才会匹配,从而无需进行显式强制转换和范围检查。
instanceof 中的基本类型
instanceof 关键字过去仅接受引用类型,自 Java 16 起,它也可以接受类型模式。现在让 instanceof 也接受基本类型是很有意义的。在这种情况下,instanceof 将检查转换是否安全,但不会实际执行转换:
该 JEP 提议支持这种结构,这使得在“类型模式检查”和“基本类型检查”之间切换变得更加容易。
switch 中的基本类型
Java 22 版本的 switch 语句/表达式支持 byte、short、char 和 int 值。该 JEP 提议增加对其他基本类型的支持:boolean、float、double 和 long。
对 boolean 值使用 switch 可以作为三元运算符 (?:) 的良好替代方案,因为其分支可以包含语句,而不仅仅是表达式。
与 Java 25 有何不同?
与 Java 25 相比,有一个微小的变化:在 switch 结构中应用了更严格的支配性检查(dominance checks)。
如果一个模式匹配了另一个模式所匹配的所有值,则称该模式支配(dominate)另一个模式。支配性 的定义过去仅适用于引用类型;本 JEP 扩大了该定义,使其也涵盖了基本类型。因此,例如,现在可以说类型模式 long q 支配类型模式 int i。
预览警告
请注意,此 JEP 处于 预览 阶段,因此您需要添加 --enable-preview 命令行标志才能试用该功能。
更多信息
有关此功能的更多信息,请阅读 JEP 530。
弃用
Java 26 还弃用了一些旧功能。让我们看看在提高稳定性和清晰度方面涉及了哪些内容。
JEP 500:准备让 final 真正成为 final
Java 中的 final 字段代表不可变状态。一旦在构造函数或类初始化器中赋值,final 字段就不能重新赋值。这种行为对于逻辑正确性和性能至关重要。类行为的约束越多,JVM 能应用的优化(如常量折叠)就越多。此外,我们对 final 字段不可变性的预期,在多线程代码的对象安全初始化中起着重要作用。
遗憾的是,“final 字段不能被重新赋值”这一预期并不总是正确的。多个 API 允许程序中的任何代码随时重新赋值 final 字段,从而破坏了正确性推断并导致重要优化失效。深度反射 API(通过 Field.setAccessible 和 Field.set 方法)是其中最臭名昭著的。这些方法允许您随意修改 final 字段。
该示例表明,在实践中,final 字段可能和非 final 字段一样可变。
修改 final 字段的原因
为什么要提供这种可能性?答案与序列化有关。序列化库需要在反序列化过程中初始化对象时修改 final 字段。问题在于,很少有代码是出于正当理由去修改 final 字段的,但仅仅因为这些 API 的存在,使得人们无法信任任何 final 字段的值。回过头来看,提供此功能是一个糟糕的选择,因为它牺牲了完整性。
最近增加的隐藏类和 Record 不允许修改 final 字段,现在是时候将这种行为扩展到普通类了。
final 字段限制
JEP 500 提议在通过深度反射修改 final 字段时发出警告。这些警告将为未来版本做好准备,该版本将通过限制 final 字段的修改来默认确保完整性,从而使 Java 程序更安全、潜在速度更快。此规则将保留一个例外:即需要在反序列化期间修改 final 字段的序列化库,通过一个有限用途的 API 进行支持。
这些最终字段限制的影响将随着时间的推移而加强。未来的 JDK 版本将默认在 Java 代码使用深度反射修改 final 字段时抛出异常,而不是发出警告。
启用 final 字段修改
应用程序开发者可以通过命令行选择启用 final 字段修改,从而避免这些警告和异常。为此,请指定 --enable-final-field-mutation 命令行选项,并传入以逗号分隔的模块名称列表:
其他技术也可使用,例如设置环境变量、将其添加到 JAR 的清单中或使用 jlink 在自定义运行时中进行配置。有关这些技术的更多详细信息,请参考 JEP 500。
Field::set 的行为
在 JDK 26 中,final 字段的 Field::set 规则发生了变化。该字段仅在以下情况被修改:
f.setAccessible(true)已经成功执行;- 该字段的声明类位于对调用者模块开放(open)的包中;
- 已为该模块启用了
final字段修改。
后两个条件是新增的。因此:
- 如果模块未启用
final字段修改,任何通过深度反射更改final字段的尝试都会抛出IllegalAccessException(除非 JVM 以--illegal-final-field-mutation启动)。f.setAccessible(true)可能仍会成功,但f.set(...)是非法的。 - 如果启用了
final字段修改,但该字段所在的包未对模块开放,则会抛出相同的异常。这可能发生在模块 A(具有开放包)调用f.setAccessible(true)并将Field传递给模块 B 时,如果模块 B 启用了修改但无法访问该包,则模块 B 的f.set(...)是非法的。
对序列化库的影响
当未来的 JDK 版本收紧 final 字段限制时,序列化库将无法自动依赖深度反射。库维护者不应要求用户通过命令行标志开启修改,而应使用 sun.reflect.ReflectionFactory API。该 API 允许序列化库获取指向特殊 JDK 生成代码的方法句柄,该代码可以通过直接写入实例字段来初始化对象,即使它们被声明为 final。生成的代码为库提供了与 JDK 内置序列化机制相同的功能,消除了为库模块启用 final 字段修改的必要性。
请注意,
sun.reflect.ReflectionFactory仅适用于反序列化实现了java.io.Serializable的类。
库和框架不应使用深度反射修改 final 字段
一些依赖注入、测试和模拟库依赖深度反射来篡改对象,甚至更改 final 字段。其维护者应仅将通过命令行开关启用 final 字段修改视为最后的手段。最好采用消除修改 final 或私有字段需求的方案。例如,大多数 DI 框架已经禁止注入 final 字段,并鼓励使用构造函数注入。
更多信息
有关此功能的更多信息,请阅读 JEP 500。
JEP 504:移除 Applet API
当 Java 平台在 90 年代末和 21 世纪初成名时,其主要催化剂之一是 Java Applet 和 Applet API。Java Applet 是可以嵌入网页并在浏览器中运行的小型 Java 程序,允许开发者创建交互式 Web 应用。它们被广泛用于游戏、动画和网页上的其他交互内容。那些根本不是 Java 程序员的人,也因为 Applet 而从浏览器中知道了“Java”这个名字。
然而,随着时间的推移,由于安全问题以及 JavaScript 和 HTML5 等替代技术的兴起,Java Applet 变得不再流行。因此,许多浏览器供应商已经取消了对它们的支持。这就是为什么 Applet API 在 Java 9 中被弃用,在 Java 17 中被弃用以备移除,这也是它在 Java 26 中被彻底移除的原因之一。最重要的是,运行 Applet 所需的沙箱化不可信代码的基础——安全管理器(Security Manager),已在 Java 24 中被永久禁用,这为最终淘汰 Applet API 提供了又一个理由。
移除内容
以下元素将被移除:
- 整个
java.applet包,包括:java.applet.Appletjava.applet.AppletContextjava.applet.AppletStubjava.applet.AudioClip
- 这些额外的类:
java.beans.AppletInitializerjavax.swing.JApplet
- 任何引用上述类和接口的其余 API 元素,包括以下类中的方法和字段:
java.beans.Beansjavax.swing.RepaintManager
风险与迁移
鉴于 Applet API 目前的形式基本上已无法使用,移除该 API 对用户应用程序没有实质性风险。仍然使用 Applet API 的应用程序要么会停留在旧版本上,要么会迁移到其他 API,例如 AWT API 或用于音频播放的 javax.sound.SoundClip 类。
更多信息
有关此项移除的更多信息,请阅读 JEP 504。
结语
以上就是 Java 26 中包含的 10 个 JEP 的讨论。但这还不是全部:此版本还包含了许多其他更新,包括各种性能、稳定性和安全性的改进。可以肯定的是:这个版本的 Java 已经为今年晚些时候的更多新特性做好了充分准备。那么,您还在等什么?是时候体验一下这个全新的 Java 版本了!