概要

長年開発が続けられてきたOpenJDKの「Project Valhalla」が、大きな節目を迎えた。JDK 28にプレビュー機能として統合されたJEP 401「値オブジェクト(プレビュー)」は、Javaに新しいvalue修飾子を導入し、アイデンティティを持たないクラスインスタンス「値クラス(value class)」を定義できるようにする。最も注目すべき変更点は、値クラスのインスタンスに対して==演算子の挙動が再定義されることだ。従来のアイデンティティ・オブジェクトでは、==は「同一のオブジェクトを参照しているか」を判定していたが、値クラスでは「同じクラスで同じフィールド値を持つかどうか」を判定するようになる。この機能はプレビュー段階のため、--enable-previewフラグを付けてコンパイル・実行する必要がある。

値クラスの仕組みと==演算子の変更

値クラスは、複素数やピクセルの色、日付のように、本質的にはデータを運ぶためだけに存在する小さなクラスを想定した機能だ。value修飾子を付けて宣言すると、すべてのインスタンスフィールドが暗黙的にfinalとなり、コンストラクタが完了するまでにすべてのフィールドへの代入を終える必要があるという厳格な初期化ルールが課される。またインスタンスメソッドの同期化(synchronized)も制限される。レコード(record)にもvalue修飾子を付けられる。

最大の変更は==の意味だ。たとえば次のようなコードでは、従来ならp1p2は別々のオブジェクトなのでp1 == p2falseになっていたが、値クラスではtrueと評価される。

Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
assert p1 == p2;   // true: 同じクラス、同じフィールド値

もっとも、これはequals()メソッドの使用を放棄する招待ではなく、「オブジェクトの比較にはequals()を使うべき」という通常のアドバイスは引き続き有効だとされている。

Project Valhallaの背景

JEP 401は、この問題を「開発者が意図することと、Javaが保証すること間の不一致」として位置づけている。複素数やピクセル色、日付を表す小さなクラスの多くは単なるデータの入れ物として使われているにもかかわらず、通常のJavaオブジェクトはアイデンティティによって定義されており、コンストラクタ呼び出しのたびに区別可能な別個のオブジェクトが生成されてしまう。この不一致を解消するため、OpenJDKの「valhalla-dev」メーリングリストで長年にわたり議論が重ねられ、今回のプレビュー実装に至った。

パフォーマンスへの最適化と注意点

値クラスの導入によって期待される主な成果は、JVMによる最適化だ。アイデンティティを持たないことで、JVMは値オブジェクトを構成フィールドに分割する「スカラー化」や、フィールドや配列要素にコンパクトな表現で直接格納する「フラット化」といった最適化を適用できるようになる。ただし、こうした最適化が常に適用されるとは限らず、適用されない場合は通常のオブジェクト割り当てにフォールバックする。フラット化はアトミック性の制約を受けるため、典型的なプラットフォームでは64ビット程度までのコード化が現実的とされる。またベンチマーク上の性能向上が保証されているわけではない点にも注意が必要だ。一方でセキュリティ面では、==identityHashCodeが間接的にプライベートフィールドの値を露出させる可能性があることや、大規模な値オブジェクトのツリー同士を比較する処理が無制限に時間を要しうることも指摘されている。

開発者への影響と今後の展望

開発者にとっては、新しいvalue修飾子の使い方を習得するだけでなく、==synchronizedの挙動変化によって予期しない動作に遭遇する可能性がある。またプリミティブラッパー型やLocalDateなど、JDKが提供する複数の「値ベース」クラスも今後この仕組みへ移行していくとみられる。関連するJEP 539「JVM内の厳格なフィールド初期化」では、バイトコード検証によって値クラスの構築ルールを強制する仕組みが導入される予定で、コンパイラの警告機能も拡充される見込みだ。今回のプレビューはJavaのオブジェクトモデルに重大な変化をもたらすものと認識されているが、開発者が実際に驚きに直面する頻度は限定的で、対応可能な範囲にとどまると見られている。