Javaのレコードクラス(record)は、「名前・金額・日付」のような値の組をそのまま運ぶための専用クラスです。フィールド・コンストラクタ・アクセサ・equals/hashCode/toStringをコンパイラが自動で用意するため、1行の宣言で、各コンポーネントのフィールドがfinalになる浅くイミュータブルなデータクラスを定義できます。
この記事では、通常のクラスとの違い、コンストラクタのカスタマイズ、継承やsetterが書けない理由とそのときのコンパイルエラー、Jackson・Spring Boot・JPAでの扱いまでを、Temurin JDK 25.0.5でコンパイル・実行した結果を載せて解説します。最後に、JDK 28で予定されている value record の挙動もEAビルドで確かめました。
まとめ
- 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と通常クラスの違い(早見表)
迷ったときに見る比較表です。Lombokの @Value も、recordの前によく使われていた選択肢として並べました。
| 観点 | record | 通常クラス | Lombok @Value |
|---|---|---|---|
| 利用条件 | JDK 16以上 | 制約なし | Lombokの導入 |
| アクセサ名 | x() |
自由 | getX() |
| setter | 書けない | 書ける | 生成しない |
| extends | 不可 | 可 | 可 |
| サブクラス化 | 不可 | 可(非final時) | 既定では不可 |
| コンポーネント以外のフィールド | staticのみ | 可 | 可 |
| equals・hashCode | 全コンポーネントで自動 | 手書き | 自動 |
| パターンで分解 | 可(Java 21以降) | 不可 | 不可 |
| JPAエンティティ | 不可 | 可 | 不向き |
新規コードで「値の組」を表すなら、Java 16以上の環境ではrecordを第一候補にしてよいと判断します。getX() 形式を前提にしたライブラリとの互換性、可変状態、継承、JPAエンティティのいずれかが要件にあるときだけ、通常クラスかLombokを選びます。
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 | 無名パターン_が正式機能 |
| 25 | 2025-09-16 | JEP 513 | コンストラクタでthis(...)より前に処理を置ける |
| 28 | 2027年3月予定 | JEP 401 | value recordがプレビュー予定 |
プレビュー版のJava 14・15では --enable-preview を付けないとコンパイルできません。2026年10月時点の最新版はJDK 27(2026-09-15 GA・非LTS)、最新のLTSはJDK 25です。record自体はLTSのJava 17から使え、record patternsまで使う新規開発ならJava 21または25が候補になります。21と25で変わった言語機能はJava 21と25の違いで比較しています。
Oracleの日本語チュートリアルとして検索上位に出る「レコード・クラス」はJava 15(プレビュー時点)の版です。現行の記述はJava 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();
}
クラスは final で java.lang.Record を継承し、フィールドはすべて private final、アクセサは public です。コンストラクタの可視性はrecord自体と同じ(この例ではパッケージプライベート)になります。アクセサ・equals・hashCode・toStringは自分で実装し直せますが、アクセサは public が必須です。
equalsの比較規則:double・null・配列
java.lang.RecordのAPIドキュメントは、参照型のコンポーネントを Objects.equals で、プリミティブ型を対応するラッパークラスの compare が0を返すかで比べると定めています。== とは結果が変わる場面があります。
record D(double v) {}
record N(String s) {}
new D(Double.NaN).equals(new D(Double.NaN)); // true (Double.NaN == Double.NaN は false)
new D(0.0).equals(new D(-0.0)); // false(0.0 == -0.0 は true)
new N(null).equals(new N(null)); // true
new N(null).hashCode(); // 0 (NullPointerExceptionにならない)
new N(null).toString(); // N[s=null]
金額や座標を double で持つrecordをキーにして 0.0 と -0.0 を同じ値として扱いたいなら、コンパクトコンストラクタで v = v + 0.0; のように正規化しておきます(-0.0 + 0.0 は 0.0 になり、正規化後は new D(-0.0).equals(new D(0.0)) がtrueになりました)。配列コンポーネントは中身でなく同一インスタンスかどうかで比べられる点が別の落とし穴で、後述の「浅い不変性」の章で扱います。
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
値を正規化したいときは、フィールドではなく引数に再代入します(例:name = name.strip();)。再代入した値がそのままフィールドに入ります。this.start = start; と書くとコンパイルエラーです。
可視性にも制約があります。正規コンストラクタはrecord自体と同じかそれ以上に公開する必要があり、public record の中で public を付けずに Range { と書くとコンパイルが通りません。上の例が 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 で失敗しました。
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 ファクトリメソッドのほうが名前で意図を示せます。任意項目が多い場合のBuilderとの組み合わせはJavaのBuilderパターン実装で扱っています。
recordで書けないこと:継承・setter・インスタンスフィールド
recordの制約は「値の組」という意味を守るためのもので、Java言語仕様 8.10に列挙されています。主な3つを先に説明し、JDK 25.0.5のjavacが出すエラー文と直し方を次の表にまとめます。
継承: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年10月時点のステータスはCandidateで、どのJDKにもプレビューとして入っていません。当面は手書きのメソッドで代替します。
インスタンスフィールド:コンポーネント以外の状態は持てません。計算で求まる値はメソッドにし、キャッシュが必要なほど重い計算ならrecordではなく通常のクラスを選びます。
コンパイルエラー12種の原因と直し方
Temurin 25.0.5で javac -J-Duser.language=en を実行したときのエラー文です。日本語環境では訳文で表示されます。
| 書いたコード | エラー文 | 直し方 |
|---|---|---|
record A(int x) extends Base |
'{' expected |
インタフェースに変える |
abstract record |
modifier abstract not allowed here |
sealed interfaceで階層を作る |
private int y; |
field declaration must be static |
コンポーネントに足す |
インスタンス初期化子 { ... } |
instance initializers not allowed in records |
コンパクトコンストラクタへ移す |
void setX(int v) { this.x = v; } |
cannot assign a value to final variable x |
withXで新インスタンスを返す |
コンパクトコンストラクタで this.x = x; |
cannot assign a value to final variable x |
引数 x に再代入する |
| 委譲しない追加コンストラクタ | constructor is not canonical, so it must invoke another constructor |
先頭か処理の後に this(...) |
public record で Q { } |
invalid canonical constructor |
public Q { } にする |
アクセサを int x() で定義 |
accessor method must be public |
public を付ける |
| 入れ子recordから外側のフィールドを参照 | non-static variable base cannot be referenced from a static context |
値をコンポーネントで渡す |
o instanceof P(int x)(Pは2要素) |
incorrect number of nested patterns |
要素数を合わせ、不要分は _ |
--release 21 で this(...) の前に処理 |
flexible constructors is not supported in -source 21 |
JDK 25以上でビルドする |
正規コンストラクタの可視性違反では、2行目に attempting to assign stronger access privileges; was public と理由が出ます。extends のエラーが「継承できません」ではなく構文エラー('{' expected)として出るのは、recordの文法に extends 句そのものが無いためです。エラー文から原因が読み取りにくい代表例なので覚えておくと調査が早く済みます。
recordの浅い不変性とList・配列の変更対策
recordが保証するのは「フィールドの参照を差し替えられない」ことまでで、参照先のオブジェクトまでは凍結しません。java.lang.Record のAPIドキュメントがrecordを「shallowly immutable(浅くイミュータブル)」と書いているのはこの意味です。
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 要素を含むリストでは NullPointerException になるので、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]
配列は Objects.equals で比べられるため、中身ではなく同一インスタンスかどうかで判定され、toString もハッシュ値の表示になります。配列を持たせるなら equals/hashCode/toString を Arrays.equals などで上書きするか、List<Byte> や専用の値クラスに置き換えます。内容比較を実装して HashSet や Map のキーに使うなら、受け取り時とアクセサで配列をコピーし、格納後に内容が変わらないようにします。
リフレクションによるrecordフィールドの変更制限
Java 25では、通常のクラスのうち隠しクラスではないクラスの非staticな final フィールドは、setAccessible(true) を通せばリフレクションで書き換えられます。recordのフィールドはこの抜け道も塞がれています。Field.setの仕様が、書き込みを許す条件に「宣言クラスがrecordでないこと」を含めているためです。
Field f = Point.class.getDeclaredField("x");
f.setAccessible(true);
f.setInt(new Point(1, 2), 9);
// IllegalAccessException: Can not set final int field ...Point.x to (int)9
// 同じ操作を通常クラスのfinalフィールドに行うと、値は9に変わった
Point.class.isRecord(); // true
for (RecordComponent rc : Point.class.getRecordComponents()) {
System.out.println(rc.getName() + ":" + rc.getType()); // x:int、y:int(宣言順)
}
getRecordComponents() はAPIドキュメントのとおりレコードヘッダーの宣言順でコンポーネントを返すので、JSONライブラリなどのフレームワークはこれと正規コンストラクタを組み合わせてrecordを生成します。リフレクションの基本はリフレクションとはを参照してください。
recordのシリアライズと復元時のコンストラクタ検証
implements Serializable を付けたrecordは、通常のクラスと復元の仕組みが異なります。通常のSerializableクラスはそのクラス自身のコンストラクタを呼ばずに状態を復元しますが、recordはJavaオブジェクト直列化仕様のとおり、ストリームから読んだ値で正規コンストラクタを呼び出して復元します。
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 が無視されます。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(_) -> ... のように _ で読み捨てられます(JEP 456)。型ごとの処理を外部に書き出すこの形は、Visitorパターンの代わりとしても使えます。
JSON・設定値・JPAにおけるrecordの対応と制約
Jackson 3でのJSON変換と2.x系との既定値の違い
Jacksonはrecordのコンポーネントと正規コンストラクタを自動で認識するため、アノテーションなしでJSONと相互変換できます。Jackson 3.2.4で実行した結果です。
record User(String name, int age, String email) {}
JsonMapper m = JsonMapper.builder().build();
m.writeValueAsString(new User("Sato", 30, "[email protected]"));
// {"name":"Sato","age":30,"email":"[email protected]"} ← 宣言順
m.readValue("{\"name\":\"Sato\"}", User.class);
// User[name=Sato, age=0, email=null] ← 欠けた項目は既定値
m.readValue("{\"name\":\"Sato\",\"age\":30,\"extra\":1}", User.class);
// User[name=Sato, age=30, email=null] ← 未知の extra は黙って無視
m.readValue("{\"start\":5,\"end\":1}", Range.class);
// ValueInstantiationException: Cannot construct instance of `Range`, problem: start > end: 5 > 1
// (Rangeは前章のコンパクトコンストラクタ付きrecord)
注意したいのは未知のプロパティの扱いです。同じJSONをJackson 2.22.4の ObjectMapper で読むと UnrecognizedPropertyException で止まりました。Jackson 3.0のリリースノートに「FAIL_ON_UNKNOWN_PROPERTIES の既定をfalseに変更」とあり、3系へ上げるとAPIのスペルミスを検知できなくなります。厳密に弾きたいなら JsonMapper.builder().enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) を明示します。3系では SORT_PROPERTIES_ALPHABETICALLY の既定もtrueになりましたが、recordのコンポーネントは上の出力のとおり宣言順で出力されました。
JSONのキー名を変えるときはコンポーネントに @JsonProperty("user_name") を付ければ、書き出しと読み込みの両方に効きます。2系と3系で依存の書き方が変わる点はJacksonをMavenで導入する手順にまとめています。
Spring Bootの@ConfigurationPropertiesによるrecordへの設定値バインド
設定値の受け取りもrecordの得意分野です。Spring Bootのリファレンスは、コンストラクタが複数ない限り、recordに @ConstructorBinding は不要としています。Spring Boot 4.1.2で次のコードを動かしました。
@ConfigurationProperties("app.mail")
public record MailProperties(String host, int port, @DefaultValue("false") boolean ssl) {
public MailProperties {
if (port <= 0) throw new IllegalArgumentException("port must be positive: " + port);
}
}
// 起動クラスに @ConfigurationPropertiesScan を付ける
// application.properties: app.mail.host=smtp.example.com / app.mail.port=587
// 結果: MailProperties[host=smtp.example.com, port=587, ssl=false]
// port=0 で起動: Failed to bind properties under 'app.mail' to demo.MailProperties
// Reason: java.lang.IllegalArgumentException: port must be positive: 0
コンパクトコンストラクタの検証が起動時に走るため、設定ミスはアプリが動き始める前に止まります。通常のクラスでも@ConfigurationPropertiesと@Validatedによる起動時検証は可能です。このrecordの例では、コンパクトコンストラクタに検証を集約し、生成後のフィールド再代入も防げます。
JPAエンティティへのrecord採用制限
Jakarta Persistenceはエンティティに引数なしコンストラクタとfinalでないクラスを求めており、Jakarta Persistence 3.2の仕様書は「An enum, record, or interface may not be designated as an entity.」と明記しています。エンティティクラスは通常のクラスで書き、画面やAPIへ渡すDTOをrecordにする分担が扱いやすい構成です。値の部品としてなら、Hibernate 6.2以降は @Embeddable としてrecordを使えます。
DTO・値オブジェクトとしての使い分け
向くのは、APIのリクエスト/レスポンスDTO、金額や期間のような値オブジェクト、設定値、メソッドから複数の値をまとめて返す戻り値、Mapのキーです。どれも「生成後に変わらず、中身が同じなら同じもの」として扱うデータです。
向かないのは、状態が変わるオブジェクトと、フレームワークが引数なしコンストラクタとsetterを要求する場面です。代表がJPAのエンティティです。もう1つの失敗パターンは、可変のListをそのまま持たせて「recordだから不変」と思い込むことです。HashMapのキーに入れた後に要素が変わると、検索で見つからなくなる場合があります。配列は既定では同一性で比較されるので要素を変えてもハッシュ値は変わりませんが、内容比較のequals・hashCodeを実装した場合は同じ問題が起こります。
層をまたぐデータをrecordに置き換えるときは、エンティティとDTOの境界を先に決めると移行が進めやすくなります。業務システムを新規に組む場合の設計から実装までの進め方は、フルスクラッチ開発のページで紹介しています。
JDK 28のvalue recordで変わる同一性(EAビルドで実測)
JEP 401(Value Objects)は2026年10月時点でStatusがIntegrated、JDK 28(2027年3月GA予定)にプレビューとして入る予定です。recordに value を付けると、インスタンスが「同一性(identity)」を持たない値オブジェクトになります。OpenJDK 28-ea+21で --enable-preview を付けて実行しました。
value record VPoint(int x, int y) {}
record IPoint(int x, int y) {}
System.out.println(new VPoint(17, 3) == new VPoint(17, 3)); // true (フィールドの状態で比較)
System.out.println(new IPoint(17, 3) == new IPoint(17, 3)); // false(別インスタンス)
Objects.hasIdentity(new VPoint(17, 3)); // false
Object o = new VPoint(17, 3);
synchronized (o) { } // 実行時に java.lang.IdentityException
// 変数の型がVPointなら、コンパイル時に unexpected type(required: identity class)
いまのrecordは equals こそ値で比べますが、== では別物で、ロックにも使えます。value recordの == は同じクラスのフィールド状態を比較しますが、equalsによる論理的な等価性と常に一致するわけではありません。また、synchronizedなど同一性を要求する操作は失敗します。--enable-preview なしでは value classes are a preview feature and are disabled by default でコンパイルできませんでした。
今のうちにできる備えは2つです。recordを == で比べるコードと、recordをロックや IdentityHashMap のキーに使うコードを無くしておくことです。どちらもvalue化した時点で挙動が変わります。プレビューの間は仕様が変わる可能性があるため、本番コードで使うのは正式化を待ってからにしてください。JDK 27時点の状況はJava 27とはでまとめています。
よくある質問
レコードクラスはJavaのどのバージョンから使えますか?
正式機能としてはJava 16からです。Java 14と15ではプレビュー機能で、--enable-preview が必要でした。record patternsまで使うならJava 21以降が必要です。
recordにsetterは書けますか?
書けません。フィールドが private final なので、代入すると cannot assign a value to final variable でコンパイルエラーになります。値を変えたいときは、変更後の値で新しいインスタンスを返す withX のようなメソッドを定義します。
recordは継承できますか?
他のクラスを extends することも、recordを親にしてサブクラスを作ることもできません。インタフェースは実装できるので、共通の型や振る舞いはインタフェースで表します。
recordのtoStringやequalsは変更できますか?
できます。同じシグネチャのメソッドを書けば自動生成分を置き換えます。配列をコンポーネントに持ち、要素の内容で等価性を判定したい場合は、equals と hashCode を整合する形で実装します。
recordとDTOはどう使い分けますか?
DTOは「層の間でデータを運ぶ」という役割の名前で、recordはそれを実装する手段の一つです。生成後に値を変えないDTOならrecordで書き、setterによる更新や継承など、recordの制約と合わない要件があるDTOは通常のクラスにします。