「Javaのクラス定義、もっとシンプルにならないかな…」
長年Javaで開発をしていると、データを保持するためだけのクラス、いわゆるDTO(Data Transfer Object)を作るたびに、同じようなコードを何度も書いていることに気づきます。フィールドを宣言し、コンストラクタを作り、getterを一つひとつ用意して、おまけにequals()やhashCode()、toString()までオーバーライドする。この作業に、私は少しうんざりしていました。
あるプロジェクトでJava14から導入されたrecordを試したところ、これまで数十行にわたって書いていたDTOが、たった1行で定義できたのです。コード量が劇的に減り、クラスの目的が「データを保持すること」だと一目でわかるようになりました。この経験は、私のJavaコーディングを大きく変えるきっかけになりました。
この記事では、recordの基本構文からclassとの違い・コンパクトコンストラクタ・Spring Bootでの実践例・使うべきケース/避けるべきケースまで、2026年時点の現場知識をまとめて解説します。
この記事は、かつての筆者と同じくrecordのメリットや書き方を知りたい方に向けて書いています。
- クラス定義の冗長さに悩んでいる
- recordの具体的な使い方が知りたい
- classとrecordをどう使い分ければ良いか分からない
こんな悩みを抱えているなら、ぜひ読み進めてください。この記事を読み終える頃には、Javaのrecordを完全に理解し、あなたのプロジェクトで自信を持って活用できるようになるでしょう。
Javaのrecordとは?【簡単に言うとデータ専用クラス】

Javaのrecordは、不変(イミュータブル)なデータを保持するための専用クラスです。
これまでデータを扱うためだけに作っていたクラスの、面倒な記述を大幅に削減してくれます。Java 14(2020年)でプレビュー機能として導入され、Java 16(2021年3月)で正式にリリースされました。2026年4月時点ではJava 17・21(LTS版)で標準的に利用できます。recordを使えば、データの定義に集中でき、より本質的なロジックの記述に時間を使えるようになります(仕様の詳細は Oracle公式の Records ドキュメント も参照)。
recordが登場した背景(Java14で導入された理由)
recordが登場する前のJavaでは、データを保持するクラスを作るのに多くの「お決まりのコード(ボイラープレート)」が必要でした。
例えば、x座標とy座標を持つ「点」を表すクラスを作るだけでも、
- フィールド(
x,y) - コンストラクタ
- 各フィールドのgetterメソッド(
getX(),getY()) equals()メソッド(値が同じか比較するため)hashCode()メソッド(ハッシュ値の計算)toString()メソッド(デバッグ用の文字列出力)
これらすべてを自分で実装する必要があったのです。これらは決まりきった作業でありながら、バグの温床にもなりやすく、開発者の負担になっていました。recordは、こうした課題を解決するために生まれました。
recordが解決する「クラス定義の冗長さ」とは
言葉で説明するよりも、実際のコードを見てもらうのが一番分かりやすいでしょう。先ほどの「点」を、従来のclassで表現してみます。
import java.util.Objects;
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[" +
"x=" + x +
", y=" + y +
']';
}
}たった2つのデータを保持したいだけなのに、40行近くのコードが必要になります。これがrecordが解決しようとした「クラス定義の冗長さ」です。
recordの基本構文と定義方法
では、同じPointクラスをrecordで定義するとどうなるのでしょうか。驚くほどシンプルになります。
public record Point(int x, int y) {}たったこれだけです。
classキーワードをrecordに変え、クラス名の後に()を付けて、保持したいデータ(フィールド)をカンマ区切りで宣言します。これだけで、先ほどの40行近いコードと全く同じ機能を持つクラスが定義できてしまいます。
具体的には、この1行のrecord定義によって、以下の要素がコンパイラによって自動的に生成されます。
驚くほど簡潔だと思いませんか。これがrecordの大きな魅力です。
recordの使い方【サンプルコード付き】

recordの基本的な魅力が分かったところで、次は具体的な使い方を見ていきましょう。インスタンスの生成やメソッドの追加など、実践的な使い方をサンプルコードと共に解説します。
最もシンプルなrecordの例
先ほどのPointレコードを実際に使ってみましょう。インスタンスの生成方法は、通常のクラスと全く同じです。
// Pointレコードの定義
public record Point(int x, int y) {}
// インスタンスの生成
var p1 = new Point(10, 20);
var p2 = new Point(10, 20);
var p3 = new Point(30, 40);
// フィールドへのアクセス
System.out.println("p1のx座標: " + p1.x()); // 出力: p1のx座標: 10
System.out.println("p1のy座標: " + p1.y()); // 出力: p1のy座標: 20
// 自動生成されたtoString()の確認
System.out.println(p1); // 出力: Point[x=10, y=20]
// 自動生成されたequals()の確認
System.out.println("p1とp2は同じか: " + p1.equals(p2)); // 出力: p1とp2は同じか: true
System.out.println("p1とp3は同じか: " + p1.equals(p3)); // 出力: p1とp3は同じか: falseこのように、インスタンス化はnewを使い、フィールドへのアクセスはフィールド名()という形式のメソッドで行います。getX()ではなくx()となる点に注意してください。
コンストラクタ・メソッドを定義する方法
recordは自動でコンストラクタを生成してくれますが、独自のコンストラクタやメソッドを追加することも可能です。
独自のコンストラクタ(コンパクトコンストラクタ)
コンストラクタで引数のバリデーションを行いたい場合があります。recordには、そのための特別な「コンパクトコンストラクタ」という構文が用意されています。
public record User(String name, int age) {
// コンパクトコンストラクタ
public User {
if (age < 0) {
throw new IllegalArgumentException("年齢は0以上である必要があります");
}
}
}
// 使い方
var user1 = new User("Taro", 25); // OK
// var user2 = new User("Jiro", -1); // ここでIllegalArgumentExceptionが発生public User { ... }のように、引数リストの()を省略して書くのが特徴です。この中でバリデーションロジックを記述します。フィールドへの代入(this.age = age;など)は、コンパイラが自動で行ってくれるため不要です。
独自のメソッド
もちろん、recordにも通常のクラスと同じように、独自のメソッドを追加できます。
public record Circle(double radius) {
// 円の面積を計算するメソッド
public double area() {
return radius * radius * Math.PI;
}
}
// 使い方
var circle = new Circle(5.0);
System.out.println("円の面積: " + circle.area()); // 出力: 円の面積: 78.5398...このように、データを保持するだけでなく、そのデータを使った簡単な計算処理などを持たせることも可能です。
interfaceを実装するrecordの例
recordは他のクラスを継承できませんが、implementsでインターフェースを実装することは可能です。データの形はrecordで、振る舞いの契約はinterfaceで表現する、というのが現代的な使い方です。
public interface Shape {
double area();
}
public record Square(double side) implements Shape {
@Override
public double area() {
return side * side;
}
}
public record Circle(double radius) implements Shape {
@Override
public double area() {
return radius * radius * Math.PI;
}
}
// 使い方
Shape s = new Square(3.0);
System.out.println(s.area()); // 9.0この方法を使えば、List<Shape> のように複数のrecordを共通のインターフェース型でまとめて扱えます。継承のかわりに「契約」で多態性を実現できる、recordと相性の良い設計パターンです。
recordとイミュータブル(不変)な設計の関係
recordを理解する上で非常に重要なのが「イミュータブル(不変)」という概念です。
recordで宣言されたフィールドは、すべてprivate finalとして扱われます。これは、一度インスタンスを作成したら、その中身(フィールドの値)を後から変更できないことを意味します。setterメソッドも自動生成されません。
なぜ不変であることが重要なのでしょうか。
不変なオブジェクトは、一度作れば状態が変わらないため、プログラムの動作を予測しやすくなります。特に、複数のスレッドから同時にアクセスされるような環境(マルチスレッドプログラミング)では、データが意図せず書き換えられる心配がなくなり、非常に安全にデータを扱うことができます。
recordは、この安全で堅牢な「不変オブジェクト」を、非常に簡単に作成するための仕組みなのです。
recordと通常のクラスの違い

recordが便利なことは分かりましたが、具体的にclassと何が違うのでしょうか。コード量以外の違いを詳しく見ていきましょう。
classとのコード量の比較
これはすでにお見せしましたが、改めて比較してみます。
【従来のclass(約40行)】
public final class PointClass {
// フィールド、コンストラクタ、getter、equals、hashCode、toString...
// (先ほどの長いコード)
}【record(1行)】
public record PointRecord(int x, int y) {}コード量は圧倒的にrecordが少なく、クラスの目的が「xとyというデータを保持すること」であることが一目瞭然です。可読性と保守性が劇的に向上します。
getter/setterの違い
recordとclassでは、フィールドへのアクセス方法が異なります。
- class (getter): 通常、
getという接頭辞がつきます。(例:point.getX()) - record (アクセサ):
getはつかず、フィールド名と同じ名前のメソッドになります。(例:point.x())
また、前述の通りrecordは不変であるため、値を変更するためのsetterメソッド(setX()など)は生成されません。 これがrecordの大きな特徴の一つです。
equalsやhashCode、toStringの自動生成について
classでは自分で実装する必要があったこれらのメソッドを、recordは自動で、かつ適切に実装してくれます。
equals(): 全てのフィールドの値が等しい場合にtrueを返します。hashCode(): 全てのフィールドの値を使ってハッシュ値を計算します。toString(): クラス名と全フィールドの値を"クラス名[フィールド名=値, ...]"という形式の分かりやすい文字列で返します。
これらのメソッドを自分で実装すると、フィールドを追加した際に修正を忘れるなどのミスが起こりがちです。recordを使えば、そうした人為的なミスを防ぐことができ、コードの安全性が高まります。
Lombok(@Data・@Value)との違いと使い分け
「コード削減ならLombokでもよくない?」とよく質問を受けます。確かにLombokの@Dataや@Valueを使えば、getter・equals・toStringを自動生成でき、見た目はrecordと近い結果になります。ただし、両者は中身がかなり違います。
| record(JDK標準) | Lombok @Value | |
| 不変性 | 強制(全field final) | @Valueなら不変、@Dataなら可変 |
| 継承 | 不可 | 可能 |
| 追加ツール | 不要(Java16+) | Lombok依存・IDEプラグイン必須 |
| アクセサ | x() 形式 | getX() 形式 |
| Builderパターン | 非対応(手書き要) | @Builderで自動生成 |
使い分けの目安は「不変なデータ保持ならrecord、可変・継承・Builderが要るならLombok」。Java16以降を採用しているプロジェクトで純粋なDTOを書くなら、外部依存のないrecordを優先するのが筋が良いです。実務で見ると、Lombokは便利な反面、IDEプラグインの更新タイミング次第でgetter/setter補完が一時的に壊れたり、annotation processorのビルド時間が地味に効いてくる場面があります。recordなら標準仕様なのでこの種のメンテ負債が乗りません。なお、JEP 468として検討中のwith構文が将来的にrecordへBuilder的な書き換え機能を追加する見込みです。
recordを使うメリット・デメリット

どんな技術にも良い面と悪い面があります。recordを効果的に使うために、メリットとデメリットの両方をきちんと理解しておきましょう。
メリット① コードが短くなる
最大のメリットは、やはりコードの記述量を大幅に削減できる点です。
DTOやVO(Value Object)など、データを保持するだけのクラスを簡潔に書けるため、開発効率が向上します。コードが短くなることは、可読性の向上にも直結し、結果としてメンテナンスしやすいプログラムに繋がるでしょう。
メリット② 不変データを安全に扱える
recordは、設計上イミュータブル(不変)です。
意図しないデータの書き換えを防げるため、プログラムの堅牢性が上がります。特にマルチスレッドで複数の処理が並行に動くシステムでは、不変性のメリットが大きく、安心してデータを使い回せます。
筆者が以前担当した銀行口座系のドメインでは、publicフィールドで残高を持っていたコードを引き継いだ際、別チームのメンバーが残高フィールドを直接書き換えるコードを混入させ、データ不整合の事故が起きたことがあります。private+getter/setterに統一したあと同種のバグは0件になりましたが、recordはそもそもfieldがprivate finalでアクセサ経由しか提供されないので、こうした「うっかり書き換え事故」を構文レベルで封じてくれます。チームに新人が増えたタイミングほど効きが大きい仕組みです。
デメリット① 継承ができない
recordは、他のクラスをextends(継承)することができません。
これは、recordが内部的にjava.lang.Recordという特別なクラスを継承しているためです。Javaではクラスの多重継承が認められていないため、他のクラスを継承できないという制約があります。ただし、implementsを使ってインターフェースを実装することは可能です。
もっとも、現場目線で言うと「継承できない」はむしろメリット側に転びがちです。親クラスが何十段にも積み上がったプロジェクトでは、処理を追うだけで疲弊し、ちょっとした修正でも影響範囲の見極めに時間が溶けます。recordが継承を禁止しているのは、データクラスを「継承で拡張するな・組み合わせ(コンポジション)で表現しろ」という設計上のガードレールとして機能していると捉えるのが、長期保守の観点では筋が良いです。
デメリット② 柔軟な拡張には向かない
recordのフィールドはすべてfinalであり、インスタンス変数(finalではないフィールド)を持つことができません。
そのため、「オブジェクト作成後に状態が変化する」ような、可変(ミュータブル)なクラスとして設計したい場合にはrecordは不向きです。recordはあくまで「変わらないデータのかたまり」を定義する機能であり、可変クラスを置き換えるものではないと押さえておくと迷いません。
recordの実践例|DTO・APIレスポンス・Spring Bootでの使いどころ

recordがどのような場面で特に役立つのか、具体的な実践例を見ていきましょう。
recordを使ったDTO(データ転送オブジェクト)の例
DTOは、異なるレイヤー間(例: サービス層とコントローラー層)でデータをやり取りするために使われるオブジェクトです。DTOの役割は純粋にデータを運ぶことなので、recordの特性と非常に相性が良いです。
例えば、ユーザー情報を運ぶUserDtoを考えてみましょう。
【classの場合】
// フィールド、コンストラクタ、getter、equals...などが必要
public class UserDto {
private final long id;
private final String name;
private final String email;
// ...
}【recordの場合】
public record UserDto(long id, String name, String email) {}recordを使えば、このように1行で定義が完了します。データベースから取得したデータを詰め替えて、APIのレスポンスとして返すような場面で大活躍します。
Spring Bootでrecordを使うケース
WebアプリケーションフレームワークのSpring Bootでは、recordがREST APIのリクエスト/レスポンスDTOとしてそのまま使えます。Jackson経由のJSON変換も自動で効くため、ボイラープレートが目に見えて減ります。
Spring Boot 3.x(2026年時点の最新系)は、recordをAPIのリクエストボディやレスポンスボディとして自動的に解釈してくれます。
@RestController
public class UserController {
// ユーザー情報を表現するrecord
public record UserResponse(long id, String name) {}
@GetMapping("/users/{id}")
public UserResponse getUser(@PathVariable long id) {
// 本来はデータベースなどからユーザー情報を取得する
// ここではダミーデータを返す
return new UserResponse(id, "Taro Yamada");
}
}このコードでは、UserResponseというrecordを定義し、それをコントローラーメソッドの戻り値にしています。Spring Bootは、このrecordオブジェクトを自動的に以下のようなJSONに変換してクライアントに返してくれます。
{
"id": 1,
"name": "Taro Yamada"
}recordのおかげで、JSONの構造とJavaのコードが1対1で対応し、非常に見通しが良くなります。
Java 21のレコードパターン(switchでの分解マッチング)
Java 21(2023年9月リリース)で正式化されたレコードパターン(Record Patterns)を使うと、switch式やinstanceofでrecordのフィールドを直接分解できます。これまで p.x()・p.y() と何度も呼んでいたコードが、宣言的に書けるようになります。
public sealed interface Shape permits Circle, Square {}
public record Circle(double radius) implements Shape {}
public record Square(double side) implements Shape {}
// Java 21 のレコードパターンで分解
public static double area(Shape s) {
return switch (s) {
case Circle(double r) -> r * r * Math.PI;
case Square(double side) -> side * side;
};
}case Circle(double r) のように、recordの中身を直接変数で受け取れます。sealed interface と組み合わせると、Java公式が想定する「データ指向プログラミング」の形になり、コードの意図が一段クリアになります。Java 21以上のプロジェクトなら積極的に採用したい機能です。
recordを使うときの注意点
recordは便利ですが、いくつか注意点もあります。
- JPAエンティティには使えない:データベースのテーブルとマッピングするJPAのエンティティとしてrecordを使うことは、2026年時点でも推奨されていません。JPAの仕様では、引数なしのコンストラクタやsetterメソッドが要求されることが多く、recordの設計と相性が悪いためです。エンティティは従来のclassで定義し、それをDTOであるrecordに変換して扱うのが良い方法です。
- 可変(ミュータブル)なオブジェクトには使わない:オブジェクトの状態が後から変わることを前提とする設計には、recordを使用してはいけません。例えば、Builderパターンで段階的にオブジェクトを構築していくようなケースや、設定情報を保持しつつ動的に変更するようなオブジェクトには不向きです。
- 何でもrecordにしようとしない:recordは銀の弾丸ではありません。データ保持という明確な目的を持つ場合にのみ使用し、複雑なビジネスロジックや状態管理が必要な場合は、これまで通りclassを使いましょう。
recordを使うべきか?まとめと判断基準
最後に、recordとclassをどのように使い分ければよいのか、判断基準をまとめます。
classとrecordの使い分けポイント
この2つは競合するものではなく、目的によって使い分ける「適材適所」の関係です。
- record を使うべき時:
- 目的: データの集合を「不変なコンテナ」としてシンプルに表現したい。
- 具体例: DTO、VO、APIのレスポンス/リクエスト、設定情報など。
- キーワード: 不変性、データ、簡潔さ
- class を使うべき時:
- 目的: 状態を持ち、その状態が変化する。または、他のクラスとの継承関係や、複雑なビジネスロジックをカプセル化したい。
- 具体例: サービス、リポジトリ、ドメインオブジェクト(状態変化を伴うもの)、JPAエンティティなど。
- キーワード: 可変性、振る舞い、拡張性
recordが向いているケース/向かないケース
| 向いているケース | 向かないケース | |
| 概要 | 主にデータを保持することが目的の場合 | 状態の変化や複雑なロジックが主目的の場合 |
| 具体例 | ・DTOやVO ・APIのリクエスト/レスポンス ・複数の値を返すメソッドの戻り値 | ・JPAエンティティ ・継承が必要な設計 ・Builderパターンなど可変性が求められる場面 |
今後のJava開発でrecordが主流になるのか
recordがすべてのclassを置き換えることはありません。しかし、データを扱うという特定の領域においては、間違いなくrecordが今後のスタンダードになっていくでしょう。
冗長なコードを減らし、コードの意図を明確にするrecordは、現代のJava開発における強力な武器です。
これまで何気なくclassで書いていたデータ保持用のクラスを、一度「これはrecordで書けないか?」と考えてみる癖をつけるだけで、あなたのJavaコードはよりクリーンで、より安全なものに進化するはずです。


