概要

業務システムの開発では、API レスポンスの格納先や画面表示用のデータ転送など、フィールドを持つだけの不変クラスを作る場面が頻繁にあります。従来の Java ではそのたびに equals・hashCode・toString・getter を手書きし、フィールドを追加するたびにこれらを修正する必要がありました。Java 16 で正式導入された record は、この定型コードをコンパイラが自動生成してくれる仕組みです。この記事では、record の基本構文とコンパクトコンストラクタによるバリデーション、メソッドの追加、DTO としての設計パターンを、動くコードで整理します。Java 8 環境で同等のクラスを書く場合との比較も示すので、移行の判断材料にもなるはずです。

使いどころ

REST API のレスポンスを UserDto record で受け取り、画面表示やログ出力に toString を活用する

CSV 取込時の1行分のデータを record で表現し、コンパクトコンストラクタで必須項目のバリデーションを行う

帳票出力用の集計結果を record に詰めて返却し、呼び出し元での意図しない書き換えを防ぐ

コード例

RecordBasicDemo.java
import java.util.List;

public class RecordBasicDemo {

    // record で DTO を定義(toString / equals / hashCode 自動生成)
    record Person(String name, int age) {}

    // コンパクトコンストラクタでバリデーション
    record ValidatedAge(int value) {
        ValidatedAge {
            if (value < 0 || value > 150) {
                throw new IllegalArgumentException(
                    "年齢は 0〜150 の範囲で指定してください: " + value);
            }
        }
    }

    // record にメソッドを追加できる
    record Rectangle(double width, double height) {
        public double area() { return width * height; }

        // with パターン: フィールドを変更した新しいインスタンスを返す
        public Rectangle withWidth(double newWidth) {
            return new Rectangle(newWidth, height);
        }
    }

    // 業務 DTO の例
    record UserDto(int id, String email, String displayName) {}

    public static void main(String[] args) {
        // 基本操作: 生成・toString・equals
        var p1 = new Person("田中太郎", 25);
        var p2 = new Person("田中太郎", 25);
        System.out.println(p1);                        // Person[name=田中太郎, age=25]
        System.out.println("equals: " + p1.equals(p2)); // true

        // コンパクトコンストラクタによるバリデーション
        try {
            new ValidatedAge(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("検証エラー: " + e.getMessage());
        }

        // メソッド追加と with パターン
        var rect = new Rectangle(5.0, 3.0);
        System.out.println("面積: " + rect.area());
        var wider = rect.withWidth(10.0);
        System.out.println("幅変更後: " + wider);

        // DTO としての一覧操作
        var users = List.of(
            new UserDto(1, "[email protected]", "田中太郎"),
            new UserDto(2, "[email protected]", "山田花子")
        );
        users.forEach(System.out::println);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record が正式に使える。コンパクトコンストラクタでバリデーションも簡潔に書ける。toString の出力形式は自動で className[field=value] になる。

Java 17
// Java 17: record で同じ機能を1行で定義
record Person(String name, int age) {}
// toString, equals, hashCode, アクセサが自動生成される

// コンパクトコンストラクタでバリデーション
record ValidatedAge(int value) {
    ValidatedAge {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException(
                "年齢は 0〜150: " + value);
        }
    }
}

Library Comparison

標準 API(record)Java 16 以上で DTO や値オブジェクトを定義する場合。依存追加なしで equals・hashCode・toString が揃う。Java 8 環境では使えない。with パターン(フィールド変更コピー)は手動で定義する必要がある。
Lombok(@Value / @Data)Java 8 環境で定型コードを減らしたいとき。@Builder によるビルダーパターンも使える。アノテーションプロセッサへの依存が増え、IDE やビルドツールとの相性問題が起きることがある。Java 16 以降では record で代替できる場面が多い。
AutoValue(Google)Lombok を避けつつ、Java 8 環境で不変値クラスを自動生成したいとき。abstract class を継承する設計が独特で、record と比べると冗長。新規プロジェクトでは record を選ぶ方が自然。

注意点

record は暗黙的に final であり、継承できない。既存の class 階層に組み込みたい場合は interface の implements を使う

コンパクトコンストラクタではフィールドへの代入を書かない。代入はコンパイラが末尾に自動挿入するため、明示すると重複代入のコンパイルエラーになる

record のフィールドはすべて final なので、値を変えたい場合は withXxx メソッドで新しいインスタンスを返すパターンを使う

record の equals は全フィールドの値比較を行う。大量の record インスタンスを HashSet や HashMap のキーにする場合、hashCode の衝突率にも注意が必要

record に static フィールドは追加できるが、インスタンスフィールドを追加することはできない。コンポーネントとして宣言したもの以外は持てない設計になっている

FAQ

record に getter を追加する場合、get プレフィックスは付けるべきですか。

record のアクセサは name() のように get なしが標準です。JavaBeans 規約が求められるフレームワーク(一部の JSP・EL 式など)では別途 getName() を定義する必要がありますが、それ以外では record 標準のアクセサを使うのが自然です。

record のフィールドを一部だけ変えた新しいインスタンスを作るにはどうすればよいですか。

withXxx メソッドを自前で定義し、新しい record を返す方法が一般的です。例えば withAge(int newAge) で new Person(name, newAge) を返します。Java 標準にはまだ with 構文はありません。

既存の class を record に置き換えるときの注意点はありますか。

record は暗黙的に final なので、サブクラスがある場合は移行できません。また equals の比較ロジックが全フィールド比較に変わるため、既存コードで一部フィールドだけで比較していた場合は動作が変わります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます