Javaプログラミングにおいて、長年開発者を悩ませてきた問題の一つに「ボイラープレートコード(定型的なコード)」の肥大化があります。

単にデータを保持するためだけのクラスを作る際にも、フィールドの定義に加えてコンストラクタ、アクセサメソッド(getter)、equals()hashCode()toString()といったメソッドを手動で実装、あるいはIDEで生成する必要がありました。

この課題を解決するために導入されたのがレコードクラス(Record Class)です。

Java 14でプレビュー版として登場し、Java 16で正式に標準機能となったこの機能は、単なるコードの短縮にとどまらず、プログラムの意図をより明確にし、データの不変性(イミュータビリティ)を担保するための強力な武器となります。

本記事では、Javaレコードクラスの基礎から応用、そして実務で欠かせないDTOでの活用法まで、プロフェッショナルの視点で詳しく解説します。

Javaレコードクラスとは?導入の背景と基本概念

Javaのレコードクラスは、「不変なデータの担い手(Transparent carriers for immutable data)」を定義するための特別な種類のクラスです。

従来のクラスが「振る舞い(メソッド)」と「状態(フィールド)」の両方を管理することに重点を置いていたのに対し、レコードは「データそのもの」を表現することに特化しています。

従来のJavaでは、データを保持するだけの「POJO (Plain Old Java Object)」を作成する際、以下のようなコードを記述する必要がありました。

Java
// 従来のPOJOクラスの例
public class User {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        // 実装が必要
    }

    @Override
    public int hashCode() {
        // 実装が必要
    }

    @Override
    public String toString() {
        // 実装が必要
    }
}

このようなコードは、本質的なデータの構造が見えにくくなるだけでなく、フィールドを追加するたびに各メソッドを更新しなければならないという保守上のリスクを抱えていました。

これに対し、レコードクラスを使用すると、わずか一行でこれら全ての機能を定義することが可能になります。

レコードクラスの基本的な書き方と自動生成される機能

レコードクラスを定義するには、classキーワードの代わりにrecordキーワードを使用します。

括弧内には、そのレコードが保持するデータ(コンポーネント)を記述します。

基本的な構文

Java
// レコードクラスの定義
public record User(String name, int age) {}

この一行の記述だけで、Javaコンパイラは以下の要素を自動的に生成します。

  1. private finalなフィールド(各コンポーネントに対応)
  2. 全フィールドを初期化するコンストラクタ(標準コンストラクタ)
  3. アクセサメソッド(コンポーネント名と同じ名前のメソッド。例:name()
  4. equals() / hashCode()メソッド(全コンポーネントを使用して判定)
  5. toString()メソッド(全コンポーネントの値を表示)

実際にどのように動作するか、以下のコードで確認してみましょう。

Java
public record User(String name, int age) {}

public class Main {
    public static void main(String[] args) {
        // インスタンス化
        User user1 = new User("Alice", 25);
        User user2 = new User("Alice", 25);

        // アクセサの使用 (getなしのメソッド名)
        System.out.println("Name: " + user1.name());
        System.out.println("Age: " + user1.age());

        // toStringの実行結果
        System.out.println("toString: " + user1);

        // equalsの動作 (値が同じならtrue)
        System.out.println("Equals: " + user1.equals(user2));
        
        // hashCodeの動作
        System.out.println("HashCode 1: " + user1.hashCode());
        System.out.println("HashCode 2: " + user2.hashCode());
    }
}
実行結果
Name: Alice
Age: 25
toString: User[name=Alice, age=25]
Equals: true
HashCode 1: 1963862961
HashCode 2: 1963862961

このように、値に基づいた等価性(Value-based equality)が最初から備わっている点が、レコードクラスの大きな特徴です。

コンストラクタのカスタマイズ(コンパクト・コンストラクタ)

レコードクラスでは、自動生成されるコンストラクタにバリデーション(値の検証)や正規化の処理を追加したい場合があります。

このとき、レコード特有の「コンパクト・コンストラクタ」という記法を利用できます。

コンパクト・コンストラクタの利用例

コンパクト・コンストラクタでは、引数リストやフィールドへの代入処理を省略して記述できます。

Java
public record Product(String name, int price) {
    // コンパクト・コンストラクタ
    public Product {
        // バリデーション処理
        if (price < 0) {
            throw new IllegalArgumentException("価格を負にすることはできません: " + price);
        }
        // 引数がnullの場合の処理(必要に応じて)
        if (name == null) {
            name = "Unknown";
        }
        // ここでフィールドへの代入コードを書く必要はない(自動で行われる)
    }
}

この記法を使うことで、ボイラープレートを排除しつつ、データとしての整合性を守るためのロジックを簡潔に記述できます。

なお、明示的に引数を取る「標準コンストラクタ」を記述することも可能ですが、その場合は全てのフィールドを初期化する必要があります。

レコードクラスを利用するメリット

レコードクラスを採用することで、開発プロジェクトには多くのメリットがもたらされます。

1. 圧倒的なコード量の削減

前述の通り、POJOで数十行必要だった記述が数行に収まります。

これは単にタイピングの手間が減るだけでなく、コードの可読性を飛躍的に向上させます。

何がデータの中心的な要素であるかが一目で理解できるようになります。

2. イミュータビリティ(不変性)の強制

レコードのフィールドは自動的にfinalとなり、セッターメソッドは生成されません。

これにより、一度生成されたインスタンスの状態が変化しないことが保証されます。

不変オブジェクトは、マルチスレッド環境においてスレッドセーフであり、バグの混入を防ぐための重要な設計指針となります。

3. セマンティクスの明確化

「これは単なるデータの器である」という意図を、クラス定義そのもので表現できます。

通常のクラスでは、将来的に複雑な状態遷移やビジネスロジックが追加される可能性がありますが、レコードとして定義されていれば、その用途がデータの保持に限定されていることが明確になります。

従来のクラスやLombokとの違い

Javaでボイラープレートを削減する手段として、古くからLombokというライブラリが使われてきました。

特に@Data@Valueアノテーションは有名ですが、レコードクラスとはいくつかの違いがあります。

機能レコードクラスLombok (@Value)従来のクラス
言語標準はい (Java 16+)いいえ (ライブラリが必要)はい
フィールドの不変性強制 (final)指定可能任意
継承不可 (finalクラス)可能可能
Getterの名前name()getName()任意
実行時のオーバーヘッドなしなしなし

Lombokは依然として「既存のクラス(継承が必要なものなど)を簡潔に書きたい」というケースで有用ですが、純粋なデータ保持目的であれば、標準機能であるレコードクラスを選択するのが現在のベストプラクティスです。

外部ライブラリへの依存を減らせる点も、大きなメリットと言えます。

レコードクラスを使用する際の注意点と制限事項

非常に便利なレコードクラスですが、設計上の制限事項も存在します。

これらを理解せずに使用すると、思わぬ制約に突き当たることがあります。

1. 継承ができない

レコードクラスは暗黙的にjava.lang.Recordを継承しており、Javaでは多重継承が禁止されているため、他のクラスを継承(extends)することはできません

また、レコード自体も暗黙的にfinalであるため、レコードを継承したサブクラスを作ることもできません。

ただし、インターフェースを実装(implements)することは可能です。

2. フィールドの追加ができない

レコードの本体({} の中)にインスタンスフィールドを追加することはできません。

全てのインスタンスデータは、ヘッダー(レコード名の後の括弧内)で定義する必要があります。

staticなフィールドやメソッドは定義可能です。

3. 「浅い不変性」に注意

レコードのフィールドがfinalであっても、そのフィールドが参照しているオブジェクト自体が可変であれば、内容を書き換えることができてしまいます。

Java
public record Team(String name, List<String> members) {}

// 注意が必要な例
List<String> mutableList = new ArrayList<>();
mutableList.add("Alice");
Team team = new Team("Alpha", mutableList);

// レコードのフィールドを通じてリストを操作できてしまう
team.members().add("Bob");

このような事態を防ぐには、コンパクト・コンストラクタ内でList.copyOf()などを使用して、変更不可能なリストとして保持するといった対策が必要です。

DTO(Data Transfer Object)としての活用法

実務において、レコードクラスが最も輝く場面の一つがDTO(Data Transfer Object)です。

APIのリクエストやレスポンス、データベースからのクエリ結果を受け取るためのオブジェクトとして、レコードは最適です。

Spring Bootでの活用例

最新のSpring Bootなどのフレームワークでは、レコードクラスをそのままJSONとの変換に使用できます。

Java
// APIのレスポンス用DTO
public record UserResponse(
    Long id,
    String username,
    String email,
    LocalDateTime createdAt
) {}

// コントローラーでの利用
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable Long id) {
        // 実際にはサービス層からデータを取得
        return new UserResponse(id, "java_user", "info@example.com", LocalDateTime.now());
    }
}

JSON出力結果

Jacksonライブラリなどの標準的なシリアライザはレコードクラスをサポートしているため、特別な設定なしで以下のようなJSONが出力されます。

JSON
{
  "id": 1,
  "username": "java_user",
  "email": "info@example.com",
  "createdAt": "2026-03-05T10:00:00"
}

DTOは基本的に「データを運ぶこと」だけが目的であり、ロジックを持つべきではありません。

この特性は、レコードクラスの設計思想と完璧に一致します。

Java 21以降の最新機能:レコードパターン

Java 21では、レコードクラスの利便性をさらに高めるレコードパターン(Record Patterns)が正式に導入されました。

これにより、instanceofswitch文において、レコードのコンポーネントを直接分解して取り出すことが可能になりました。

レコードパターンのコード例

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

public class PatternMatch {
    public static void printPoint(Object obj) {
        // instanceofとレコードパターンの組み合わせ
        if (obj instanceof Point(int x, int y)) {
            System.out.println("座標: x=" + x + ", y=" + y);
        } else {
            System.out.println("Point型ではありません");
        }
    }
}

以前は、一度Point型にキャストしてからアクセサメソッドを呼ぶ必要がありましたが、パターンマッチングを使うことで「型判定」と「値の抽出」を一気に行えるようになります。

これは、複雑なデータ構造を扱う際のコードを劇的に簡潔にします。

まとめ

Javaレコードクラスは、単なるボイラープレートの削減手段を超え、Javaにおけるデータモデリングの在り方を根本から変える機能です。

レコードクラスを導入することで得られる主なメリットを振り返ると以下の通りです。

簡潔な記述

一行でコンストラクタからアクセサまで定義可能。

不変性の担保

安全で予測可能なコード設計を促進。

標準機能の安心感

Lombokなどの外部ツールに頼らず、言語標準でデータを表現。

最新機能との親和性

パターンマッチング(Java 21)による強力なデータ操作。

一方で、継承ができない点や、コレクションを保持する際の「浅い不変性」には注意が必要です。

特にDTOや値オブジェクト(Value Object)としての利用において、レコードクラスは現在のJava開発において必須の知識と言っても過言ではありません。

もし、あなたのプロジェクトに「データを保持するだけなのに、なぜか100行以上あるクラス」が存在するなら、それはレコードクラスへリファクタリングする絶好のチャンスです。

最新のJavaが提供するこの強力な機能を活用し、よりクリーンで保守性の高いコードを目指しましょう。