Javaのレコードクラス(record)は、「名前・金額・日付」のような値の組をそのまま運ぶための専用クラスです。フィールド・コンストラクタ・アクセサ・equals/hashCode/toStringをコンパイラが自動で用意するため、1行の宣言で、各コンポーネントのフィールドがfinalになる浅くイミュータブルなデータクラスを定義できます。
この記事では、宣言の書き方とコンストラクタのカスタマイズ、継承やsetterが書けない理由、DTOやJPA・Jacksonでの扱いまでを、Temurin JDK 25.0.4.1でコンパイル・実行した結果を載せて解説します。
まとめ
- recordはJava 16(JEP 395)で正式機能になりました。Java 14・15ではプレビュー扱いです
record Point(int x, int y) {}と書くと、private finalフィールド・正規コンストラクタ・x()形式のアクセサ・equals/hashCode/toStringが生成されます- 引数の検証や正規化は「コンパクトコンストラクタ」に書きます。フィールドへの代入は書けず、引数を再代入します
- recordは暗黙に
java.lang.Recordを継承するfinalクラスなので、extendsもサブクラス化もできません。インタフェースの実装はできます - コンポーネントのフィールドは再代入できませんが、参照先の不変性は保証されません。配列は必要に応じて受け取り時とアクセサでコピーし、リストは
List.copyOfで防御的コピーを取ります - DTO・値オブジェクト・メソッドの複数戻り値に向きます。JPAのエンティティにはできません
以下、宣言の基本から順に、実行結果とともに確認します。
recordが追加された経緯と対応バージョン
recordは「データを運ぶだけのクラスに、毎回コンストラクタ・getter・equals・hashCode・toStringを手書きしている」という冗長さを解消するために追加されました。JEP 395はrecordを「不変データの透過的な運搬役」と説明しています。ボイラープレートの削減そのものより、「このクラスは値の組である」という設計意図をコンパイラが強制できる点が本質です。
| JDK | GA日 | JEP | recordの状態 |
|---|---|---|---|
| 14 | 2020-03-17 | JEP 359 | プレビュー |
| 15 | 2020-09-15 | JEP 384 | 第2プレビュー |
| 16 | 2021-03-16 | JEP 395 | 正式機能 |
| 21 | 2023-09-19 | JEP 440 | record patternsが正式機能 |
| 22 | 2024-03-19 | JEP 456 | 無名パターン_が正式機能 |
プレビュー版のJava 14・15では --enable-preview を付けないとコンパイルできません。2026年9月時点の最新版はJDK 27(2026-09-15 GA・非LTS)、最新のLTSはJDK 25です。record自体はJava 16から正式機能として使え、LTSではJava 17でも利用できます。record patternsも使う新規開発では、両方が正式機能になっているJava 21または25を候補にできます。
recordの宣言と自動生成されるメンバー
宣言とインスタンス生成の書き方
クラス名の後ろの丸括弧に「レコードコンポーネント」を並べます。インスタンスは通常のクラスと同じく new で生成し、値の取り出しはコンポーネント名と同じ名前のメソッドで行います。この記事のコードは抜粋なので、実行するときは public record を同名のファイルに置き、実行文をmainメソッドへ移し、必要な型をimportしてください。
public record Point(int x, int y) {}
Point p = new Point(1, 2);
System.out.println(p.x()); // 1
System.out.println(p); // Point[x=1, y=2]
System.out.println(p.equals(new Point(1, 2))); // true
アクセサは getX() ではなく x() です。JavaBeansの命名を前提にしたライブラリ(古いJSPのEL式や一部のマッピングツール)はrecordのアクセサを認識しないことがあるので、導入前に確認してください。
public record は通常のクラスと同様にファイル名と一致させます。クラスの内側に書く入れ子のrecordは private record にでき、暗黙にstaticになるため外側のインスタンスを参照しません。メソッドの中で宣言する「ローカルrecord」も書けるので、Streamの中間結果を一時的に束ねる用途に使えます。
javapで見る自動生成の中身
入れ子の record Point(int x, int y) {} をコンパイルし、javap -p で逆アセンブルした結果です。
final class Main$Point extends java.lang.Record {
private final int x;
private final int y;
Main$Point(int, int);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public int x();
public int y();
}
読み取れるのは次の4点です。クラスは final で java.lang.Record を継承していること、フィールドはすべて private final であること、コンストラクタの可視性はrecord自体と同じ(この例ではパッケージプライベート)であること、アクセサは public であることです。equals は全コンポーネントを比較し、toString は Point[x=1, y=2] 形式の文字列を返します。
アクセサ・equals・hashCode・toStringは自分で実装でき、正規コンストラクタも明示できます。ただしアクセサを実装するときは public が必須で、付け忘れると invalid accessor method in record(accessor method must be public)でコンパイルエラーになります。
recordのコンストラクタの書き方
コンパクトコンストラクタによる検証・正規化
引数チェックは、引数リストを省いた「コンパクトコンストラクタ」に書くのが定石です。本体の実行後、コンパイラが引数の値をフィールドへ代入します。
public record Range(int start, int end) {
public Range {
if (start > end) {
throw new IllegalArgumentException("start > end: " + start + " > " + end);
}
}
}
new Range(5, 1); // IllegalArgumentException: start > end: 5 > 1
コンパクトコンストラクタの中で this.start = start; と書くと cannot assign a value to final variable start でコンパイルエラーです。値を正規化したいときは、フィールドではなく引数に再代入します(例:name = name.strip();)。再代入した値がそのままフィールドに入ります。
もう1つの注意点は可視性です。正規コンストラクタはrecord自体と同じかそれ以上に公開する必要があり、public record の中で public を付けずに Range { と書くと、invalid canonical constructor in record Range(attempting to assign stronger access privileges; was public)でコンパイルエラーになります。上の例が public Range { になっているのはこのためです。
正規コンストラクタの明示と追加コンストラクタ
コンポーネントと同じ引数リストのコンストラクタ(正規コンストラクタ)を丸ごと書くこともできます。この場合は this.x = x; を全フィールドについて自分で書きます。検証だけならコンパクト形式のほうが代入漏れが起きません。
別の引数リストのコンストラクタを追加する場合は、this(...) により他のコンストラクタへ委譲しなければなりません。Java 25で正式化されたFlexible Constructor Bodies(JEP 513)により、構築途中のインスタンスに触れない処理であれば、その呼び出しより前に引数の検証や計算を置けます。下の Money(String) を --release 21 でコンパイルすると flexible constructors is not supported in -source 21 で失敗しました。委譲せずに書くと constructor is not canonical, so it must invoke another constructor で弾かれます。
public record Money(long amount, String currency) {
Money(long amount) { this(amount, "JPY"); } // 委譲が必須
Money(String text) { // Java 25以降
long v = Long.parseLong(text.strip()); // this(...)より前に処理を置ける
this(v, "JPY");
}
static Money yen(long amount) { return new Money(amount, "JPY"); } // staticファクトリは自由
}
引数の組み合わせが多いなら、コンストラクタを増やすより static ファクトリメソッドのほうが名前で意図を示せます。
recordで書けないこと:継承・setter・インスタンスフィールド
recordの制約は「値の組」という意味を守るためのものです。JDK 25のjavacで試した結果をまとめます。
| 書いたコード | 結果 |
|---|---|
record A(int x) extends Base {} |
構文エラー('{' expected) |
implements Comparable<A> |
可 |
private int y;(インスタンスフィールド) |
field declaration must be static |
static int count;(staticフィールド) |
可 |
void setX(int v) { this.x = v; } |
cannot assign a value to final variable x |
| インスタンスメソッド・staticメソッドの追加 | 可 |
継承:recordは暗黙に java.lang.Record を継承しており、Javaは単一継承なので extends の書く場所がありません。record自体も暗黙に final なので、recordを親にしたサブクラスも作れません。共通の振る舞いを持たせたいときはインタフェースを実装させます。
setter:フィールドが final なので値を書き換えるメソッドは書けません。「一部だけ変えた値」が欲しいときは、新しいインスタンスを返す withX メソッドを自分で書きます。
public record Point(int x, int y) {
public Point withX(int newX) { return new Point(newX, y); }
}
この with 相当を言語機能にする提案がJEP 468(Derived Record Creation)ですが、2026年9月時点のステータスはCandidateで、プレビュー機能としても入っていません。当面は手書きのメソッドで代替します。
インスタンスフィールド:コンポーネント以外の状態を持たせることはできません。計算で求まる値はメソッドにし、キャッシュが必要なほど重い計算ならrecordではなく通常のクラスを選びます。
recordの浅い不変性とList・配列の変更対策
recordが保証するのは「フィールドの参照を差し替えられない」ことまでで、参照先のオブジェクトまでは凍結しません。java.lang.Record のAPIドキュメントがrecordを「浅くイミュータブル(shallowly immutable)」と説明しているのはこの意味です。実測した3つのケースを示します。
Listコンポーネントの外部変更と防御的コピー
record Team(String name, List<String> members) {}
List<String> src = new ArrayList<>(List.of("a"));
Team t = new Team("dev", src);
src.add("b"); // 生成元のListを変更
t.members().add("c"); // アクセサ経由で変更
System.out.println(t); // Team[name=dev, members=[a, b, c]]
生成元のリストを変えても、アクセサで取り出したリストを変えても、recordの中身が変わりました。コンパクトコンストラクタで members = List.copyOf(members); と防御的コピーを取ると、生成後の変更は反映されず、t.members().add(...) は UnsupportedOperationException になります。List.copyOf は要素自体を複製しないため、可変な要素を持つ場合はその変更対策も必要です。また、null 要素を含むリストを受け付けないため、null を許す設計なら Collections.unmodifiableList(new ArrayList<>(members)) を使います。
配列コンポーネントの同一性比較と内容比較
record Bytes(byte[] data) {}
new Bytes(new byte[]{1}).equals(new Bytes(new byte[]{1})); // false
new Bytes(new byte[]{1}).toString(); // Bytes[data=[B@129a8472]
自動生成の equals は参照型コンポーネントを Objects.equals 相当で比べるため、配列は中身ではなく同一インスタンスかどうかで判定されます。toString もハッシュ値の表示になります。配列を持たせるなら equals/hashCode/toString を Arrays.equals などで上書きするか、List<Byte> や専用の値クラスに置き換えるのが安全です。配列内容による重複排除を期待する場合、HashSetやMapではこの比較方式の違いに注意が必要です。内容比較を実装してキーに使うなら、受け取り時とアクセサで配列をコピーするなど、格納後に内容が変わらない設計も必要です。
recordのシリアライズと復元時のコンストラクタ検証
implements Serializable を付けたrecordは、通常のクラスと復元の仕組みが異なります。通常のSerializableクラスでは、そのクラス自身のコンストラクタを呼ばずに状態を復元するのに対し、recordはストリームから読んだ値で正規コンストラクタを呼び出して復元します。
record Range(int start, int end) implements Serializable {
Range {
if (start > end) throw new IllegalArgumentException();
System.out.println("compact ctor called: " + start + "," + end);
}
}
// new Range(1, 3) を書き出して読み戻した実行結果
// compact ctor called: 1,3 ← 生成時
// compact ctor called: 1,3 ← 復元時にも呼ばれる
復元時にもコンパクトコンストラクタが実行されるため、改ざんされたバイト列で start > end の不正なインスタンスを作ることはできません。安全でないデシリアライゼーションの対策として、不変条件をコンストラクタに集約できる点はrecordの実利です。
その代わり、recordでは writeObject・readObject・readObjectNoData・serialPersistentFields が無視されます。JDK 25で private void readObject(...) を書いたrecordを復元したところ、readObject は呼ばれず、readResolve だけが呼ばれました。直列化の形式を細かく制御していた既存クラスをrecordへ置き換えると、互換性が崩れる可能性があります。
record patternsによる値の分解(Java 21以降)
Java 21で正式化されたrecord patterns(JEP 440)を使うと、instanceof や switch でrecordのコンポーネントを直接取り出せます。シールクラス(sealed)と組み合わせると、分岐の網羅漏れをコンパイラが検出します。
sealed interface Shape permits Circle, Square {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
}; // default不要。Shapeの実装を増やすとここがコンパイルエラーになる
}
area(new Square(3)) の実行結果は 9.0 です。使わないコンポーネントはJava 22以降なら case Circle(_) -> ... のように _ で読み捨てられます。型ごとの処理を外部に書き出すこの形は、Visitorパターンの代わりとしても使えます。
DTO・値オブジェクトとしての使い分け
record・通常クラス・Lombokの@Valueの比較
| 観点 | record | 通常クラス | Lombok @Value |
|---|---|---|---|
| 依存 | JDK 16以上 | なし | Lombok |
| アクセサ名 | x() |
自由 | getX() |
| サブクラス化 | 不可 | 可(非final時) | 既定では不可(変更可) |
| 非finalのインスタンスフィールド | 不可 | 可 | @NonFinalで可 |
| パターンで分解 | 可 | 不可 | 不可 |
| JPAエンティティ | 不可 | 可 | 不向き |
新規コードで「値の組」を表すなら、Java 16以上の環境ではrecordを第一候補にしてよいと判断します。getX() 形式のAPIとの互換性に加え、可変状態・継承・JPAエンティティなどの要件がある場合は、Lombokや通常クラスを検討します。
recordが向く場面と向かない場面
向くのは、APIのリクエスト/レスポンスDTO、金額や期間のような値オブジェクト、メソッドから複数の値をまとめて返す戻り値、Mapのキーです。どれも「生成後に変わらず、中身が同じなら同じもの」として扱うデータです。
向かないのは、状態が変わるオブジェクトと、フレームワークが引数なしコンストラクタとsetterを要求する場面です。代表がJPAのエンティティです。Jakarta Persistenceはエンティティにpublicまたはprotectedの引数なしコンストラクタとfinalでないクラスを求めており、Jakarta Persistence 3.2の仕様書はrecordをエンティティにすることを明示的に禁じています。エンティティクラスは通常のクラスで書き、画面やAPIへ渡すDTOをrecordにする分担が扱いやすい構成です。Hibernate 6.2以降は @Embeddable な値型としてrecordを使えます。
JSONとの相互変換は、Jackson 2.12以降ならrecordのコンストラクタとアクセサを自動で認識します。Spring Bootの @ConfigurationProperties も、2.6以降はコンストラクタが1つだけのrecordなら @ConstructorBinding なしで設定値をバインドできます。
よくある質問
レコードクラスはJavaのどのバージョンから使えますか?
正式機能としてはJava 16からです。Java 14と15ではプレビュー機能で、--enable-preview が必要でした。record patternsまで使うならJava 21以降が必要です。
recordにsetterは書けますか?
書けません。フィールドが private final なので代入するとコンパイルエラーになります。値を変えたいときは、変更後の値で新しいインスタンスを返す withX のようなメソッドを定義します。
recordは継承できますか?
他のクラスを extends することも、recordを親にしてサブクラスを作ることもできません。インタフェースは実装できるので、共通の型や振る舞いはインタフェースで表します。
recordのtoStringやequalsは変更できますか?
できます。同じシグネチャのメソッドを書けば自動生成分を置き換えます。配列をコンポーネントに持ち、要素の内容で等価性を判定したい場合は、equals と hashCode を整合する形で実装します。
recordとDTOはどう使い分けますか?
DTOは「層の間でデータを運ぶ」という役割の名前で、recordはそれを実装する手段の一つです。生成後に値を変えないDTOならrecordで書き、setterによる更新や継承など、recordの制約と合わない要件があるDTOは通常のクラスにします。