Skip to content
微信扫码关注公众号

JEP 390: Warnings for Value-Based Classes | 基于值的类的警告

摘要

将原始包装类指定为 基于值的 类,并弃用其构造函数以便后续移除,同时触发新的弃用警告。对在 Java 平台中任何基于值的类的实例上进行不当同步操作提供警告。

动机

Valhalla 项目 正在寻求以“原始类”的形式对 Java 编程模型进行重大增强。此类声明其实例为无身份且能够进行内联或扁平化表示,在这些表示中,实例可以在内存位置之间自由复制,并且仅使用实例字段的值进行编码。

原始类的设计和实现已经足够成熟,我们可以有信心地预期在未来的版本中,Java 平台的某些类将迁移为原始类。

迁移的候选类在 API 规范中被非正式地指定为 “基于值的类”。一般来说,这意味着它们编码不可变对象,其标识对类的行为并不重要,并且它们不提供实例创建机制,例如每次调用都承诺唯一标识的公共构造函数。

基本包装类(java.lang.Integerjava.lang.Double 等)也打算成为原始类。这些类满足被指定为基于值的大多数要求,但它们公开了自 Java 9 起已弃用的公共构造函数。通过对定义进行一些调整,它们也可以被视为基于值的类。

基于值的类的客户端通常不会受到原始类迁移的影响,除非它们违反了这些类的使用建议。特别是,当在未来的 Java 版本上运行时,其中已经发生了迁移:

  1. 相等(根据 equals)的这些类的实例也可能被视为相同(根据 ==),这可能会破坏依赖于 != 结果以获得正确行为的程序。

  2. 使用 new Integernew Double 等而不是隐式装箱或调用 valueOf 工厂方法来创建包装类实例将产生 LinkageError

  3. 尝试在这些类的实例上进行同步将产生异常。

这些变化对某些人来说可能不方便,但解决方法很简单:如果你需要一个标识,可以使用不同的类——通常是你自己定义的类,但 ObjectAtomicReference 也可能是合适的。迁移到原始类的好处——更好的性能、可靠的相等语义、统一基本类型和类——将完全值得这种不便。

(1)已经通过在基于值的类的工厂方法中避免关于唯一标识的承诺而不被鼓励。没有一种实际的方法可以自动检测忽略这些规范并依赖当前实现行为的程序,但我们预计这种情况很少见。

我们可以通过弃用包装类构造函数以进行删除来阻止(2),这将放大在编译对这些构造函数的调用时出现的警告。现有 Java 项目的很大一部分(可能是其中的 1%-10%)调用包装类构造函数,尽管在许多情况下它们仅打算在 Java 9 之前的版本上运行。许多流行的开源项目已经通过从其源代码中删除包装构造函数调用来响应 Java 9 的弃用警告,并且鉴于“弃用以进行删除”警告的紧迫性增加,我们可以预期会有更多的项目这样做。“依赖项”部分描述了缓解此问题的其他功能。

我们可以通过在编译时和运行时实现警告来阻止(3),以通知程序员他们的同步操作在未来的版本中将不起作用。

描述

java.lang 中的基本包装类(ByteShortIntegerLongFloatDoubleBooleanCharacter)已被指定为基于值的类。“基于值的类的描述” 已更新,以允许弃用的构造函数和实习工厂,并更好地与原始类迁移的要求保持一致(例如,基于值的类不应继承任何实例字段)。

为了阻止对基于值的类实例的误用:

  • 基本包装类构造函数在 Java 9 中最初被弃用,现在已被弃用以进行删除。在源代码中调用这些构造函数的任何地方,默认情况下 javac 会产生“删除”警告。jdeprscan 工具可用于识别二进制文件中对弃用 API 的使用。

  • javac 实现了一个新的警告类别“同步”,该类别识别对基于值的类类型的操作数或其所有子类型都指定为基于值的类型使用 synchronized 语句的情况。该警告类别默认处于打开状态,可以使用 -Xlint:synchronization 手动选择。

  • HotSpot 实现了对在基于值的类实例上发生的 monitorenter 的运行时检测。命令行选项 -XX:DiagnoseSyncOnValueBasedClasses=1 将把该操作视为致命错误。命令行选项 -XX:DiagnoseSyncOnValueBasedClasses=2 将通过控制台和 JDK 飞行记录器事件打开日志记录。

编译时的同步警告依赖于静态类型,而运行时警告可以响应对非基于值的类和接口类型(如 Object)的同步。

例如:

java
Double d = 20.0;
synchronized (d) {... } // javac 警告和 HotSpot 警告
Object o = d;
synchronized (o) {... } // HotSpot 警告

如果在 synchronized 语句或方法之外调用 monitorexit 字节码和 Object 方法 waitnotifynotifyAll,它们始终会抛出 IllegalMonitorStateException。因此,不需要对这些操作发出警告。

识别基于值的类

在 JDK 中,@jdk.internal.ValueBased 注解用于向 javac 和 HotSpot 表明一个类是基于值的,或者一个抽象类或接口需要基于值的子类。

@ValueBased 应用于 Java 平台 API 和 JDK 中的以下声明:

  • java.lang 中的基本包装类;

  • java.lang.Runtime.Version 类;

  • java.util 中的“可选”类:OptionalOptionalIntOptionalLongOptionalDouble

  • java.time API 中的许多类:InstantLocalDateLocalTimeLocalDateTimeZonedDateTimeZoneIdOffsetTimeOffsetDateTimeZoneOffsetDurationPeriodYearYearMonthMonthDay,以及在 java.time.chrono 中:MinguoDateHijrahDateJapaneseDateThaiBuddhistDate

  • java.lang.ProcessHandle 接口及其实现类;

  • java.util 中集合工厂的实现类:List.ofList.copyOfSet.ofSet.copyOfMap.ofMap.copyOfMap.ofEntriesMap.entry

只要该注解应用于一个抽象类或接口,它也会应用于 JDK 中的所有子类。

java.lang.constantjdk.incubator.foreign 中的一些类和接口声称是基于值的,但不符合修订后的要求——例如,它们继承实例字段——因此不能迁移为原始类。在这种情况下,不再适合将这些描述为基于值的类,并且它们的规范已经修订。

变更范围

Java SE:这个 JEP 通过细化基本包装类、现有的基于值的类以及相关接口和工厂方法的规范来修改 Java SE。它还弃用基本包装类构造函数以进行删除。它不会对 Java 语言或 Java 虚拟机规范进行任何更改。

JDK:在 JDK 中,这个 JEP 还为 javac 和 HotSpot 添加了新的警告和日志记录功能。并且它定义了注解 @jdk.internal.ValueBased 并将其应用于许多 JDK 类。

替代方案

我们可以放弃将这些类迁移为原始类的努力。然而,当我们完成迁移时,开发人员将享受到显著的好处,并且对依赖有问题行为的开发人员的相对影响很小。

可以用运行时警告来补充编译时的弃用警告。这留作未来另一个 JEP 的工作(见下文)。

可能还有其他类可以迁移为原始类,包括 API 类和由像 java.lang.invoke.LambdaMetafactory 这样的特性生成的类。这个 JEP 仅限于包装类和已经被指定为基于值的类。同样,额外的警告可以作为未来的工作引入。

依赖项

将基于值的类迁移为原始类需要在有这些警告的情况下有合理的准备时间。最重要的是,在这个 JEP 完成后的一些版本之后,才能进行将包装类变为原始类的 JEP。

使包装类成为原始类的另一个先决条件是有足够的工具来识别和解决对其构造函数的遗留使用。两个后续功能将在单独的 JEP 中进行研究:

  • HotSpot 对使用弃用 API(包括包装类构造函数)的运行时警告。这将补充由 javacjdeprscan 产生的警告。

  • 支持执行无法更新其对包装类构造函数使用的二进制文件的工具。例如,这可能会给程序员提供重写字节码以使用 valueOf 工厂方法的选项。