markdown

はじめに (対象読者・この記事でわかること)

この記事は、Javaの基本的な文法は理解しているが「オブジェクト型を値型で宣言できるのか」について疑問を持っている中級者以上のプログラマを対象としています。読み進めることで、Javaにおけるプリミティブ型と参照型の本質的な違い、ラップクラスやレコード・値型の概念、そして実際に「値型的に」扱うテクニックとその制限が明確に把握でき、コード設計時の型選択に自信を持って適用できるようになります。執筆のきっかけは、チーム内で「オブジェクトを値型のように扱いたい」といった要望が頻出したため、公式の仕様と実装例をまとめる必要があったことです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。
- Javaの基本的な文法(クラス・メソッド・変数宣言)
- プリミティブ型(int, long, double など)とラッパークラス(Integer, Long, Double など)の違い
- Java 14 以降のレコード(record)に関する基本概念

Javaにおける型の概念と背景

Javaは設計当初、メモリ安全性とガベージコレクションの容易さを重視し、プリミティブ型参照型の二層構造を採用しました。プリミティブ型はスタック上に直接値が格納され、コピー時に値そのものが渡ります。一方、参照型はヒープ上にオブジェクトが生成され、その参照(ポインタ)が変数に格納されます。この違いは性能面だけでなく、== 演算子の意味やhashCode/equals の実装にも影響します。

近年、Javaは値型(value type)という概念を検討・実装し始めました。JEP 395(レコードのプレビュー)やJEP 429(スレッド局所オブジェクト)で示されたように、「不可変かつスレッド安全な軽量オブジェクト」を価値のように扱う方向性が進んでいます。実際に「オブジェクト型を値型で宣言」できるかどうかは、言語仕様とJVMの実装次第です。本セクションでは、Javaの型体系と、現在の標準仕様でどこまで「値型的」な宣言が可能かを整理します。

オブジェクト型を「値型」的に扱う具体的手法

ステップ1:ラッパークラスとオートボクシングを利用する

Javaではプリミティブ型と対応するラッパークラス(例:intInteger)が用意されており、オートボクシングにより暗黙的に変換が行われます。これにより、次のようにオブジェクトとして扱いつつ、ほぼプリミティブと同等の性能が期待できます。

Java
Integer count = 10; // オートボクシングで int → Integer int primitive = count; // アンボクシングで Integer → int

しかし、内部的にはIntegerオブジェクトはヒープに格納され、== 比較は参照の同一性を評価します。したがって「真の値型」とは言い難く、コピー時に参照が渡る点でプリミティブとは挙動が異なります。

ステップ2:レコード(record)で不変データキャリアを作る

Java 16 以降、record キーワードで簡潔に 不変データキャリア を定義できます。レコードは自動的にequalshashCodetoString を生成し、全フィールドがfinalであるため「値」的に扱えるという利点があります。

Java
public record Point(int x, int y) { }

この Point は以下のように利用できます。

Java
Point p1 = new Point(3, 5); Point p2 = new Point(3, 5); System.out.println(p1.equals(p2)); // true

レコードは実体がヒープ上に生成されるものの、コピー時に全フィールドが新しいオブジェクトにコピーされるため、参照型の典型的な共有問題が軽減されます。さらに、JEP 426(スレッド局所オブジェクト)で提案された「価値型オブジェクト」の実装に向け、将来的にレコードが仮想マシンレベルでの最適化(例えばスタック上配置)を受ける可能性があります。

ハマった点やエラー解決

1. オートボクシングが原因のNullPointerException

Java
Integer num = null; int primitive = num; // ここで NPE が発生

原因nullInteger をアンボクシングしようとしたため。
対策OptionalInt などのラッパーを使用するか、明示的にnull チェックを行う。

2. レコードの不変性が期待通りに機能しないケース

Java
record MutablePair(int a, int b) { } MutablePair pair = new MutablePair(1, 2); // 直接フィールドを書き換えることはできないが、参照先オブジェクトが可変の場合問題になる

原因:レコードのフィールドが参照型で、その参照先が可変オブジェクトだった。
対策:フィールドに不変オブジェクト(Stringjava.time 系)を使用するか、ディープコピーを実装する。

解決策

  • オートボクシングの安全な使用null になる可能性がある場合はOptionalIntObjects.requireNonNullElse を活用し、意図しない NPE を防止します。
  • レコードでの不変性確保:フィールドに不変型(プリミティブ・Stringjava.time 系)を限定し、外部からの変更が入り込まないよう設計します。また、必要に応じて手動でcloneファクトリーメソッドを用意し、深いコピーを提供します。
  • JVM の将来プレビュー:JDK 22 以降でプレビューされている「価値型 (value type)」機能を試すことで、ヒープ上のオブジェクトをスタック上に最適化する実験が可能です。-XX:+UseCompressedOops などのフラグと組み合わせると、メモリフットプリントが大幅に削減されます。

まとめ

本記事では、Java においてオブジェクト型を「値型」のように扱う手段と、その限界を整理しました。

  • ラッパークラスとオートボクシングは最も手軽だが、内部実装は依然として参照型。
  • レコードは不変性と自動生成されたメソッドにより、値型的な利用が容易になる。
  • JVM の将来予定(価値型実装)は、真のスタック上オブジェクト配置を目指し、性能と安全性の両立を狙っている。

これらを理解することで、型選択の最適化とバグ回避が可能になり、コードの可読性・保守性が向上します。次回は、JDK のプレビュー機能を使った「価値型」実装例とベンチマーク結果を深掘りした記事を予定しています。

参考資料