この記事について
2020/3/16にリリースされたJava16の言語仕様に関わる変更について,自分の理解を深めるのを兼ねてまとめてみます.プレビュー中の機能は含みません.
- record
- instanceof パターンマッチング
- 値ベースのクラスに対する警告(この記事)
値ベースのクラスに関しては,Java16で直ちにコンパイル結果が変わったりするわけではありませんが,今後のバージョンで互換性を破壊する変更が入りそうなので,早めに警告をだすことにしたようです.
出典
概要
- Project Valhalla の準備である
- プリミティブ型のラッパークラスのコンストラクタを使うコードに警告を出す
- 標準ライブラリの値ベースのクラスを
synchronized
で使っているコードに警告を出す
Project Valhalla
従来のJavaのデータは,プリミティブ型と参照型の2つにはっきりと分かれています.このようなメモリ上のデータ表現を拡張するために検討・準備を行っているのが,Valhalla Projectです.主に,
- 値型(JEP 169): プリミティブ型のように使えるユーザー定義型を実現する(メモリ配置の効率化など)
- 総称型の特殊化(JEP 218): 型変数にプリミティブ型を使えるようにする(
List<int>
など?)
という内容が検討されているようです.
Java16では前者の値型に関係する変更があります.
プリミティブクラス
前述の値型のことだと思われます1.特徴として,
- 「"identity-free"であるように宣言できる」
- 参照アドレスに依存せず,内容のみが意味を持つ
- プリミティブクラスの値どうしを比較するときは,アドレスの比較ではなく内容の比較を行う
- コード上は
==
と書いてあっても,実際にはequals
に相当する比較が行われるようになる見込み
- コード上は
- 「インライン,あるいは平坦化された表現を持てる」
- 例えば配列であれば,フィールドの値だけを並べるメモリ配置になったりするのでしょうか?
この結果,
- 「インスタンスをメモリ間で自由にコピーできる」
- 「インスタンスのフィールドの値のみを使ってエンコードできる」
ようになり,コードが分かりやすくなることや,パフォーマンスの改善が期待されます.
値ベースのクラス
「値ベースのクラス(value-based class)」とは,以前から存在したクラス設計の考え方です.クラスが値ベースである要件は,
- インスタンスフィールドが全て
final
である(内容は可変オブジェクトへの参照であってもよい) -
equals
,hashCode
,toString
がインスタンスフィールドの値のみから計算されるように実装されている- ID(アドレス)にはよらない
- 全てのメソッドは,equalなインスタンスを完全に置き換え可能なように扱う
- 2つのインスタンス
x
,y
がequals
により等価と判定されるならば,x
とy
を入れ替えても結果が変わらない
- 2つのインスタンス
- インスタンスを同期モニターに使用しない
- 外部からアクセス可能なコンストラクタを持たない
- 経過措置として,
@Deprecated
指定のコンストラクタは許容
- 経過措置として,
- 「呼び出し毎に一意のIDのインスタンスを返す」ことが保証されたファクトリメソッドを持たない
- 別のファクトリメソッド呼び出しで得られたインスタンスであっても,
equals()
によって等しいならば==
によっても等しい可能性を排除できない
- 別のファクトリメソッド呼び出しで得られたインスタンスであっても,
- クラスが
final
である -
Object
か抽象クラスを直接継承する - 祖先の抽象クラスはすべて,インスタンスフィールド・インスタンス初期化子を持たず,コンストラクタが空である
以上を満たすことです2.
java.util.Optional
など一部の標準ライブラリクラスのAPIドキュメントでは,以前から値ベースのクラスであることが明記されています.
非互換な変更
Javaの標準ライブラリのうち値ベースであるものを,将来のバージョンでプリミティブクラスに変更することが予定されています.これは,これらのクラスが適切でない使われ方をしている場合に互換性のない変更となります.
プリミティブ型のラッパークラス(java.lang.Integer
等)も,プリミティブクラスへの変更が予定されています.ラッパークラスは値ベースの要件の大半を満たしていますが,public
なコンストラクタ3が存在します.
プリミティブクラスへの変更により,次のような影響が想定されます.
-
==
と書くとequal
相当の比較が行われる -
public
コンストラクタが削除される - 同期モニタに使用するとコンパイル時エラーあるいは実行時例外が発生する
既存コードへの影響
値ベースのクラスをプリミティブクラスに移行しても基本的には使う側への影響はないと考えられますが,次のような使い方をしている場合には注意が必要です:
- 比較演算子
==
!=
を使用している- 比較結果が変わります
- ラッパークラスのコンストラクタ(
new Integer
等)を呼び出している-
LinkageError
が発生します
-
- インスタンスを
synchronized
の対称として使用している- 例外が発生します
比較演算については自動的に不適切な使用を検出するのは難しいですが,あまり問題にはならないと考えられています.
JDK16から追加される警告
16の時点では互換性を損なう変更はまだ入りません4が,ユーザー向けに新しい警告が出るようになります.
- ラッパークラスのコンストラクタに関しては,従来から非推奨でしたがさらに
forRemoval=true
とすることで,コンパイル時にデフォルトで"removal"
警告が表示されます. - 同期での使用に関しては,新しい警告カテゴリ
"synchronization"
が追加されました.synchronized
文のオペランドの型5が値ベースのクラスであるか,全てのサブクラスが値ベースであるクラスの場合に警告が発生します."synchronization"
警告はコンパイル時にデフォルトで表示されます. - HotSpotは値ベースクラスのインスタンスに対する
monitorenter
の呼び出しを検出します.このとき,コマンドラインオプションXX:DiagnoseSyncOnValueBasedClasses
によってこの事象を記録したりfatal errorにすることが出来ます.
値ベースのクラスの識別
コンパイラや仮想マシンが標準ライブラリ内の値ベースのクラス6には,@jdk.internal.ValueBased
アノテーションが付与されます.対象クラスは以下の通りです.
-
java.lang
のプリミティブラッパークラス(Byte
,Short
,Integer
,Long
,Float
,Double
,Boolean
,Character
) -
java.lang.Runtime.Version
クラス -
java.util
のオプショナル系統のクラス(Optional
,OptionalInt
,OptionalLong
,OptionalDouble
) -
java.time
API の多くのクラス(Instant
,LocalDate
,LocalTime
,LocalDateTime
,ZonedDateTime
,ZoneId
,OffsetTime
,OffsetDateTime
,ZoneOffset
,Duration
,Period
,Year
,YearMonth
,MonthDay
) -
java.time.chrono
の一部のクラス(MinguoDate
,HijrahDate
,JapaneseDate
,ThaiBuddhistDate
) -
java.lang.ProcessHandler
インターフェースとその全ての実装クラス -
java.util
のコレクションファクトリ(List.of
,List.copyOf
,Set.of
,Set.copyOf
,Map.of
,Map.copyOf
,Map.ofEntries
,Map.entry
)で使用されるコレクションの実装クラス
java.lang.constant
や jdk.incubator,foreign
には以前から値ベースであると規定されていたクラス・インターフェースがありましたが,フィールドを継承しているなどJava16での値ベースの要件を満たさないため,プリミティブクラスには移行できません.このため,これらのクラスの仕様から「値ベースのクラスである」という記述は削除されました.
今後の経過措置について
今回警告を出し始めたところなので,既存の値ベースクラスをプリミティブクラスに移行するまでには,何回かのJavaリリースの間待つ必要があります.また,
- ラッパークラスのコンストラクタ呼び出しを実行時に呼び出しを検出する手段
- ラッパークラスのコンストラクタを呼び出している既存バイナリを利用できるようにする7ためのツール
を別のJEPで提供することが計画されています.
-
文献によって異なる用語が使われています.まだ検討段階の要素なので,用語も安定していないのでしょうか… ↩
-
Java15までの規定とは少し変わっています.ラッパークラスを含めるために非推奨なコンストラクタが許可され,一方でプリミティブクラスに移行するためにインスタンスフィールドの制限が追加されています. ↩
-
Java9から非推奨にはなっていました ↩
-
言語仕様・仮想マシン仕様への変更はない ↩
-
静的に決定できる範囲の型.Object型変数に代入してから使用している場合などはコンパイル時警告にはならない ↩
-
正確には,それ自体が値ベースであるクラス及び,全てのサブクラスが値ベースであるべきと規定されている抽象クラス・インターフェース ↩
-
バイトコードを書き換えて
valueOf
呼び出しに変更するなど ↩