Valhalla (5): 解读 JEP 390 Warnings for Value-Based Classes
解读 JEP 390 Warnings for Value-Based Classes,基于值的类(value-based)的相关警告。
以直译为主,部分内容会意译,一些地方会增加自己的解读。一些概念解释见 深入理解 Valhalla (0): 序言。
系列文章(未完待续)
摘要 Summary
将包装类(java.lang.Integer
、java.lang.Double
等)设计为为基于值的类,并在构造函数的 @Deprecated
中加入 forRemoval
(在 JDK 9 中已经被标记为 @Deprecated
)。在任何基于值的类的实例上进行同步(synchronized
)时,会发出警告。
可以看下最新的源代码,增加了 forRemoval
https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/lang/Integer.java
1 | /** |
动机 Motivation
Valhalla 项目希望通过引入原始类(primitive class)来对 Java 编程模型进行重大改进。原始类的实例是 Identity-free 的,能够进行内联和扁平化的表示。这样的实例可以在内存不同位置之间自由复制,也可以仅仅用字段的值进行编码(这里编码可能指内存中的布局)。
原始类的设计和实现已经足够成熟了。我们可以相信在未来的版本中,一些 Java 平台的类将会迁移成原始类。
设计和实现已经足够成熟了,相关的 JEP 都已经进入候选,可以参考后续文章。
迁移的候选者在 API 规范中被非正式的指定为基于值的,这意味着
- 不可变的
- 对象的 Identity 对自身的行为来说不重要(就是之前说的 Identity-free)
- 不提供生成独立 Identity 实例的机制,例如公开的构造函数(每次调用返回的对象都是一个新的 Identity)
非正式的指不改变任何特性,但会设计为符合相关约定,方便后续迁移。
包装类(java.lang.Integer
、java.lang.Double
等)也打算成为原始类,这些类都满足上述的要求,除了具有公开的构造函数(在 JDK 9 中已经被标记为 @Deprecated
),通过一些修改,包装类依旧可以被当作是基于值的。
基于上述原因,后续包装类的构造函数都会被移除。
基于值的类是一个抽象概念,原始类是一个具体的实现,后续不加严格区分可以认为说的是同一个事情。
一般来说,使用者如果按照建议使用,迁移成原始类之后是不会有什么影响的。在后续版本的的 JDK 中运行时,需要注意
- 相等(equals)的实例也是相同的(==),如果代码中依赖 != 就有可能产生不正确的结果。
- 使用
new Integer
、new Double
等创建实例的时候会产生LinkageErrors
,需要改成使用valueOf
工厂方法。 - 尝试在这些类的实例上进行同步将产生异常。
这些改变对一些人来说可能不方便,但解决方法也很简单,如果需要 Identity 可以使用不同的类型(通常可以自定义的类,使用 Object
或者 AtomicReference
也是可以的)。但带来的好处也是非常明显的,更好的性能、更可靠的相等语义、可以统一基本类型和类。对于上述 3 个问题:
- 已经通过工厂方法的唯一性,避免了上述问题。但没有一种实用的手段,可以监测程序忽略了这种规范。不过预计这种情况会非常少。
- 在构造函数上增加
forRemoval
,当使用上述构造方法时编译器会给出更多的警告。现有的 Java 项目有很大一部分(可能占其中的 1%-10%)调用了上述构造方法,但它们更多都只会运行在 Java 9 之前的版本上。大部分流行的开源项目,都已经根据警告信息进行了修改。增加forRemoval
之后,更多的项目也会进行相应的修改。在后文依赖部分(Dependencies)中介绍了其他的功能,会更好的帮助迁移。 - 在编译和运行时的时候增加相关的警告,通知程序员同步(
synchronized
)操作在未来的版本中将不起作用。
描述 Description
java.lang
中的包装类(Byte
、Short
、Integer
、Long
、Float
、Double
、Boolean
和 Character
)已被指定为基于值的。
为了防止基于值的实例被错误的使用:
- 包装类的构造函数,已经被标记为废弃,并且增加了
forRemoval
。javac 在编译时默认都会提示警告信息。jdeprscan 工具也可以识别二进制文件中使用已废弃 API 的情况。 - javac 新增了同步(
synchronized
)警告类型,在基于值的类或者所有子类都是基于值的类的实例上使用同步时会给出警告。默认是开启的,可以通过-Xlint:synchronization
手动选择。 - HotSpot 实现了一个运行时检测手段,可以检测在基于值的实例上使用
monitorenter
指令。命令行选项-XX:DiagnoseSyncOnValueBasedClasses=1
会将操作视为致命错误。命令行选项-XX:DiagnoseSyncOnValueBasedClasses=2
将通过控制台和 JDK Flight Recorder 事件打开日志记录。
编译时同步警告依赖于静态类型,而运行时警告可以处理非基于值的类和接口类型如 Object
,例如:
1 | Double d = 20.0; |
在同步代码之外执行 monitorexit
指令,或者 wait
、notify
和 notifyAll
本身就会抛出 IllegalMonitorStateException
,因此这些操作不需要再进行警告。
标识基于值的类 Identifying Value-Based Classes
在 JDK 中,@jdk.internal.ValueBased
注解用于向 javac 和 HotSpot 表明类是基于值的,或者表明抽象类或接口需要基于值的子类。
Java API 和 JDK 中下列类型被标记为 @ValueBased
- 包装类
java.lang.Runtime.Version
java.util
中的 optional 类:Optional
、OptionalInt
、OptionalLong
和OptionalDouble
java.time
中的 API:Instant
、LocalDate
、LocalTime
、LocalDateTime
、ZonedDateTime
、ZoneId
、OffsetTime
、OffsetDateTime
、ZoneOffset
、Duration
、Period
、Year
、YearMonth
、MonthDay
、MinguoDate
、HijrahDate
、JapaneseDate
和ThaiBuddhistDate
java.lang.ProcessHandle
接口和实现类java.util
中的集合工厂方法的实现类:List.of
、List.copyOf
、Set.of
、Set.copyOf
、Map.of
、Map.copyOf
、Map.ofEntries
和Map.entry
在抽象类或者接口中使用该注解,意味着所有子类也使用了该注解。
部分在 java.lang.constant
和 jdk.incubator.foreign
中的类和接口一开始要求是基于值的,但并不符合最新的规范(例如继承了字段),所以不能迁移成原始类。这种情况下这些类不再是基于值的。
影响范围 Scope of Changes
Java SE:优化了包装类、现有的基于值的类、相关的接口和工厂方法的规范,修改了 Java 相关的 API。对于 Java 语言规范和 Java 虚拟机规范,没有任何修改。
JDK:为 javac 和 HotSpot 增加了新的警告和日志记录的功能,一些类增加了 @jdk.internal.ValueBased
注解。
其他选择 Alternatives
我们可以放弃将这些类迁移为原始类的努力。但当我们完成迁移时,开发人员将享受到显着的好处,并且对依赖问题行为的开发人员的影响相对较小。
可以用运行时警告补充编译时弃用警告,这留作另未来一个 JEP 的工作(见下文)。
可能还有其他类可以迁移为原始类,包括一些 API 类和由 java.lang.invoke.LambdaMetafactory
等功能生成的类。该 JEP 将自身限制为包装类和已指定为基于值的类。同样,可以在未来的工作中引入额外的警告。
依赖 Dependencies
基于值的类迁移为原始类,在增加这些警告之后依然需要充分的准备时间。最重要的是,在该 JEP 完成后的一段时间内,无法将包装类迁移为原始类。
包装类迁移为原始类的另一个前提,是有足够的工具来识别和解决废弃构造函数的使用。将在单独的 JEP 中两个后续功能:
- 新增 HotSpot 运行时警告,检测已弃用 API 的使用(包括包装类构造函数),作为 javac 和 jdeprscan 的补充。
- 新增工具,支持运行无法重新编译的二进制文件(其中使用了废弃的构造函数)。例如可以重写字节码以使用
valueOf
方法。(这里应该还包含 JNI 二进制文件)
Valhalla (5): 解读 JEP 390 Warnings for Value-Based Classes