跳到主要内容

JEP 390:基于值的类的警告

QWen Max 中英对照 JEP 390: Warnings for Value-Based Classes

总结

将原始包装器类指定为基于值的,并弃用其构造函数以进行移除,从而引发新的弃用警告。针对在 Java 平台中任何基于值的类的实例上进行不当同步尝试的行为,提供警告。

动机

Valhalla 项目 正在追求对 Java 编程模型的一项重要增强,即以 primitive classes(原始类)的形式进行改进。这类类声明它们的实例是无身份标识的,并且能够以内联或扁平化的方式表示,其中实例可以在内存位置之间自由复制,并且仅通过实例字段的值进行编码。

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

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

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

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

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

  2. 尝试通过 new Integernew Double 等方式创建包装类实例,而不是使用隐式装箱或调用 valueOf 工厂方法,将会产生 LinkageError 错误。

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

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

(1) 已经通过避免在基于值的类的工厂方法中承诺唯一标识来阻止。目前尚无切实可行的方法来自动检测那些忽略这些规范并依赖当前实现行为的程序,但我们预计这种情况会很少见。

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

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

描述

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

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

  • 原始包装类构造函数最初在 Java 9 中已被弃用,现在则被标记为计划移除。无论在源代码中何处调用这些构造函数,默认情况下 javac 都会产生 removal 警告。可以使用 jdeprscan 工具来识别二进制文件中已弃用的 API 使用情况。

  • javac 实现了一个新的警告类别 synchronization,它标识了将 synchronized 语句用于基于值的类类型或其所有子类型都被指定为基于值的类型的用法。该警告类别默认开启,也可以通过 -Xlint:synchronization 手动选择启用。

  • HotSpot 实现了对基于值的类实例上发生 monitorenter 的运行时检测。命令行选项 -XX:DiagnoseSyncOnValueBasedClasses=1 会将该操作视为致命错误。命令行选项 -XX:DiagnoseSyncOnValueBasedClasses=2 将启用日志记录,包括通过控制台和通过 JDK Flight Recorder 事件记录。

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

例如:

Double d = 20.0;
synchronized (d) { ... } // javac warning & HotSpot warning
Object o = d;
synchronized (o) { ... } // HotSpot warning

monitorexit 字节码以及 Object 类的 waitnotifynotifyAll 方法在 synchronized 语句或方法之外调用时,总是会抛出 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 工厂方法的选项。