PR

【Java】レコード(record)の基本を1からわかりやすく

Java
  • レコード(record) は、主にデータを格納・転送するための軽量なクラスです。
  • 従来のクラスのように equals()hashCode(), toString() を自分で書かなくても、自動的に作ってくれます。
  • データを単純に持ち運びたいだけなら、従来のクラスよりもずっと短いコードで済みます。

最初に気になるのが、「レコード」(英語では “record”)と呼ばれるものを「レコードクラス」と呼んでいる説明もあれば、「record」とだけ呼ぶ場合もあります。

  • 結論としては、Java公式ドキュメントや仕様書においては "Record Classes" という用語が使われています。
  • ただし実際にコードを書くときのキーワードは record です。
  • 「レコードクラス」という呼び方は、Javaにおけるクラスの一種であることを強調する表現ですし、「レコード」と略して呼ぶのも広く浸透しています。要するに、「recordクラス」「レコードクラス」「レコード」のどれを使っても、基本的には同じ機能を指していると思ってOKです。

ただし、「レコード」と言った場合に、データベースの行(レコード)を想像する人もいるでしょうから、Java言語仕様として正式に呼ぶときは「レコードクラス (Record Classes)」という名前が正確だと捉えておけば安心です。

スポンサーリンク

Java:レコード(record)の概要

Javaでは長らく「データのやり取りに使うだけのクラス」を定義するのが煩雑でした。たとえば、DTO(Data Transfer Object)やValue Object(値オブジェクト)として、単に名前や年齢などのフィールドだけ持つクラスを定義したい場合にでも、以下のようなメソッドを書き足す必要がありました。

  • コンストラクタ
  • getter/setter
  • equals() / hashCode()
  • toString()

「全部自動生成してしまえば一瞬じゃないか」と思うかもしれませんが、ツールやIDEの機能を活用しても、何らかの手間はどうしてもつきまといます。そこで、Java言語においても「冗長なコードを削減し、単純にデータを表現するのに特化したクラスが欲しい」という要望が高まりました。

その結果として、Java 14(プレビュー)で初登場し、Java 16で正式導入されたのがレコード(record)です。

レコードが解決しようとしている問題

レコードの最大の目的は「ボイラープレートコード(決まりきった定型コード)の削減」にあります。特にDTO用途で「単にデータを保持しているだけ」という構造を明示化し、かつメソッドの自動生成をしてくれます。結果として、コード量が減り、ソースが読みやすくなる効果が期待できます。

また、レコードは不変(イミュータブル)な設計を前提とするため、値オブジェクト的な使い方を推奨している点も大きな特徴です。

レコードの基本構文:record

レコードクラスを宣言するには record キーワードを使います。例えば、次のように書くと、名前(String)と年齢(int)を持つレコードを定義できます。

public record Person(String name, int age) {
}

ポイントとなるのは、括弧内の引数が、そのままレコードのフィールドとして扱われることです。上記の宣言だけで、以下のものが自動生成されます。

  1. private final String name;
  2. private final int age;
  3. すべてのフィールドを受け取るコンストラクタ
  4. name() / age() というアクセサメソッド(getter相当)
  5. equals() / hashCode() / toString()

実際に内部的に自動生成されるのが↓。

public final class Person extends java.lang.Record {
    private final String name;
    private final int age;

    // コンストラクタ
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // アクセサ(getter)
    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    // equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person other = (Person) o;
        return age == other.age && java.util.Objects.equals(name, other.name);
    }

    // hashCode
    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, age);
    }

    // toString
    @Override
    public String toString() {
        return "Person[name=" + name + ", age=" + age + "]";
    }
}

ご覧のように、内部的にはjava.lang.Recordを継承したfinalなクラスとして定義され、同時に各種フィールドや必要なコンストラクタ・メソッドが定義される、という仕組みです。ひとまず↑のコードが理解できれば、レコードクラスに関する基本は抑えられたと思ってOK!

フィールドの取り扱い(アクセサメソッド)

内部的にはフィールド名と同じメソッドが使えるようになるので・・・以下のようにフィールドを参照することができます。

Person person = new Person("Alice", 20);

System.out.println(person.name()); // "Alice"
System.out.println(person.age());  // 20

getterの代わりに「フィールド名と同じメソッド」が生成されるのがレコードの特徴です。なお、レコードの場合は「setter」がありません。※レコードは基本的に不変オブジェクト(後述)を想定しているため。

レコードがもたらす利点

ボイラープレートの削減

先ほど述べたように、レコードではgetter, equals, hashCode, toStringなどを自分で書かなくて済むのが最大の利点です。ちょっとしたDTOをたくさん定義したいとき、余計なコードの量が削減され、メンテナンス性も上がります。

もともと↓のように記載していたのが、たった1~2行で定義できちゃいます、というのがレコードクラスの美味しいところです。

public final class Person extends java.lang.Record {
    private final String name;
    private final int age;

    // コンストラクタ
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // アクセサ(getter)
    public String name() {
        return name;
    }

    public int age() {
        return age;
    }

    // equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person)) return false;
        Person other = (Person) o;
        return age == other.age && java.util.Objects.equals(name, other.name);
    }

    // hashCode
    @Override
    public int hashCode() {
        return java.util.Objects.hash(name, age);
    }

    // toString
    @Override
    public String toString() {
        return "Person[name=" + name + ", age=" + age + "]";
    }
}

不変オブジェクトとしての設計

レコードは通常のクラスとは異なり、フィールドが暗黙的にfinal として扱われます。つまり、オブジェクトを生成した時点でフィールドは確定し、あとから値を変更できません。
これにより、「値が変わらない」という前提で設計することが容易になり、マルチスレッド環境などでの安全性も高まります。

「意図」が明確化される

普通のクラスは、何でもできてしまう柔軟性を持ちます。そのため、「このクラスは本来ただのデータを入れるだけのはずなのに、いつの間にかロジックが増えすぎている…」といった事態が起こりがちです。
一方、レコードは明示的に「データ保持用」であることを表すものなので、実装する人にも使う人にも、その意図がわかりやすく伝わります。

レコードを使う際の注意点

イミュータブルであること

レコードは不変オブジェクトとして扱われることを想定しているため、基本的にはフィールドは再代入できないようになっています。
ただし、フィールドが参照型で、内部に可変状態を持っている場合(例:ListMap など)には注意が必要です。レコード自身が保持する参照先のオブジェクトを通じて、内部データを変更できてしまう可能性があります。
深い意味での完全な不変性を担保したい場合は、フィールドに不変コレクションを設定したり、コンストラクタ内で防御的コピーを行ったりする工夫が必要です。

継承はできない

レコードは暗黙的に final 扱いとなり、サブクラスとして継承することはできません
Javaのオブジェクト指向の文脈では「継承がクラス設計の基本手段」という面もありますが、レコードは「あくまでデータの容れ物である」という思想により、クラス継承は不要だろうという考えに基づいています。
もし、継承による多態性が必要なら、レコードよりも通常のクラスを使うことが多いでしょう。

参照型フィールドの取り扱い

先ほど少し触れましたが、レコードのイミュータブル性は「フィールドへの再代入ができない」という点を保証するだけで、オブジェクト内部までは制御していません。

  • 例えば、public record MyData(List<String> list) と書いた場合、list 自体は後から他のオブジェクトに差し替えられませんが、list の内容を変更する(list.add("someValue")など)は可能です。
  • これを防ぎたいなら、Collections.unmodifiableList(...) を用いるなど、工夫が必要です。

コンストラクタ周りの詳細

レコードでは「すべてのフィールドを引数に取るコンストラクタ」参考 コンストラクタとは?)が自動的に定義されます。先ほどの Person であれば new Person(String name, int age) というコンストラクタがすでに用意されます。

public final class Person extends java.lang.Record {
    private final String name;
    private final int age;

    // コンストラクタ
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

ただし、このコンストラクタに関しては、覚えておきたい必須知識がいくつかありますので、ここで解説しておきます。

カノニカルコンストラクタ

括弧内に宣言したフィールドの組み合わせと同じシグニチャのコンストラクタを「カノニカルコンストラクタ」と呼びます。

public record Person(String name, int age) {
    public Person(String name, int age) {
        // ここに処理を書いてもOK(必ず this.name = name; などの代入をしなければならない)
        this.name = name;
        this.age = age;
    }
}

先ほども解説した通り、カノニカルコンストラクタはレコードクラスを定義すると自動的に生成されますが、これをあえて自分で定義することも可能です。自分で定義するときは、レコードが自動生成するコードを「上書き」するイメージで記述していけばOKで、自分でバリデーションや初期化処理を細かく書きたいときに使います。

ただし、注意しなければいけないのが、カノニカルコンストラクタを利用するときは、必ずカノニカルコンストラクタ内ですべてのフィールドを初期化する必要がある!という点です。レコード特有の制限になるので頭に入れておきましょう。

public record Book(String title, int price) {

    // カノニカルコンストラクタを明示的に定義
    public Book(String title, int price) {
        // 追加ロジック(例:タイトルが空文字ならエラー)
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("タイトルは必須です");
        }
        if (price < 0) {
            throw new IllegalArgumentException("価格は0以上である必要があります");
        }

        // 必須:レコードのすべてのフィールドを初期化
        this.title = title;
        this.price = price;
    }
}

上記のように全コンポーネントをパラメータとして受け取り、その値で必ずフィールドを初期化する必要があります。

コンパクトコンストラクタ

レコードには、さらに便利な「コンパクトコンストラクタ」という書き方があります。
これは
シグニチャがカノニカルコンストラクタと同一
(= 全フィールドを引数に取る)である点は変わりませんが、メソッドシグニチャを省略して書ける構文です。

public record Book(String title, int price) {

    // コンパクトコンストラクタ(シグニチャ省略)
    public Book {
        if (title == null || title.isBlank()) {
            throw new IllegalArgumentException("タイトルは必須です");
        }
        if (price < 0) {
            throw new IllegalArgumentException("価格は0以上である必要があります");
        }
        // title, price への代入は暗黙的に行われる
        // → this.title = title; this.price = price; 
    }
}

コンパクトコンストラクタでは、括弧内に引数を再度書かなくていい(すでにレコードで宣言してあるから)という簡略化がなされています。ここで書かれた処理は、最終的にレコードのフィールドを初期化する「カノニカルコンストラクタ」に統合されるイメージです。データに対するバリデーションや前処理を入れる場合、コンパクトコンストラクタが非常に便利です。

  1. カノニカルコンストラクタ
    • レコードのコンポーネントをすべて引数に取り、すべてのフィールドを初期化するコンストラクタ。
    • これを自動生成するだけでなく、明示的に定義してカスタマイズもできる。
  2. コンパクトコンストラクタ
    • カノニカルコンストラクタを省略形で書く方法。
    • パラメータリストを省略しつつ、必要なバリデーション等を記述できる。
    • こちらもフィールドの初期化は必須だが、コンパイラが暗黙的に this.xxx = xxx; を行ってくれる。

レコードの活用例

DTO(データ転送オブジェクト)としての利用

最も典型的な利用方法のひとつが、DTO(Data Transfer Object)としての利用です。
アプリケーション内で複数の値をまとめて別のメソッドやモジュールへ引き渡すとき、単にフィールドを持つためだけのクラスを作ることがあります。そこにレコードが大変便利です。

// 例: 顧客情報DTO
public record CustomerDto(String name, String email, String address) {
}

こうしておけば、わざわざgetterやequals()などを書かずに済みますし、「ああ、これはただのデータ入れ物なんだな」という意図を明示できます。

Web APIレスポンスの格納

サーバーサイドJavaやクライアント側でWeb APIをコールして、その結果返ってきたJSONやXMLなどをオブジェクトとして受け取る機会はよくあります。
このとき、少し前まではPOJO(Plain Old Java Object)やLombokで生成したクラスを使うことが主流でしたが、Java 16以降なら気軽にレコードを使えます。

public record UserResponse(String username, int age, String city) {
}

JacksonやGsonといったライブラリも、(バージョンや設定によりますが)レコードをある程度認識して扱えるようになっています。シリアライゼーションやデシリアライゼーションの際に、コンストラクタ引数を使う形で解釈してくれます。

小規模な値オブジェクトの表現

ドメイン駆動設計(DDD)の文脈などで、値オブジェクトという単位が存在します。これらは通常、同値性を判定するための equals()hashCode() が欠かせません。さらに、値オブジェクトはイミュータブルであることが推奨されます。
レコードで定義すれば、その2点(同値性メソッドとイミュータブル設計)をあまり意識せずに自動で満たせるため、値オブジェクトを気軽に作成できます。

レコードの制限事項・よくある疑問

サブクラスは作れない?

先ほども触れましたが、レコードは暗黙に final なクラスとして扱われるため、継承によるサブクラス化は不可能です。
もしどうしても似たようなレコード同士を共通インターフェースでまとめたいなら、インターフェース(あるいはsealed interface)を実装する形をとることは可能です。

public sealed interface Animal permits Cat, Dog {
    String name();
}

public record Cat(String name) implements Animal {}
public record Dog(String name) implements Animal {}

equals(), hashCode(), toString()のカスタマイズはどうなる?

レコードでは、フィールドに基づいた equals()hashCode()toString() が自動生成されます。
しかし、どうしてもカスタマイズしたいケースがあるかもしれません。その場合、通常のクラスと同じように自分でメソッドをオーバーライドできます。

public record Person(String name, int age) {
    @Override
    public String toString() {
        return "名前=" + name + ", 年齢=" + age;
    }
}

ただし、「レコードは純粋に値を表現する」ことが目的ですから、むやみにメソッドを上書きするのは設計上あまり好ましくない場合もあります。必要性をよく検討したうえで行うのがよいでしょう。

レコードの可変フィールドは?

レコードのフィールドは暗黙的に private final となるため、可変(var)なフィールドは基本的に定義できません。そのため、「後から値を変えたい」ケースや「大きなオブジェクトを段階的に組み立てたい」ケースにはレコードは不向きです。

どうしても可変が必要なら、通常のクラスを使うか、あるいはビルダーパターンを検討するほうが適切な場合が多いです。

レコードは「単にデータを表すだけの軽量クラス」をとてもシンプルに表現できる仕組みです。Lombokのように外部ライブラリに頼らなくても、Java言語標準だけでボイラープレートを削減できる点は大きなメリットといえます。

また、レコードはイミュータブルな値オブジェクトの表現とも非常に相性が良いため、DDD(ドメイン駆動設計)の文脈や並行処理の多いアプリケーションでも有効です。ただし、継承ができないという仕様や可変データ構造を扱うときの注意など、いくつかの制限はあるので、何でもかんでもレコードにすればいいわけではありません。

ぜひ、Javaのバージョンが16以上を使える環境であれば、従来のJavaクラスで煩雑だった部分をレコードに置き換えてみて、そのシンプルさと読みやすさを実感してみてください。適切に使い分けることで、ソースコードが洗練され、メンテナンスがしやすくなるはずです。

タイトルとURLをコピーしました