PR

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

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

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

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

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

スポンサーリンク

レコードの概要

レコードが導入された背景

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

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

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

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

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

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

レコードの基本構文

レコードの宣言と自動生成されるメソッド

レコードクラスを宣言するには 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()

つまり、普通のクラスだったら自分で書く必要のあるコードを、ほとんどレコードが肩代わりしてくれるわけです。

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

上記のレコード Person を使うときは、こんな風に書きます。

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をたくさん定義したいとき、余計なコードの量が削減され、メンテナンス性も上がります。

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

レコードは通常のクラスとは異なり、フィールドが暗黙的に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 record Person(String name, int age) {
    public Person(String name, int age) {
        // ここに処理を書いてもOK(必ず this.name = name; などの代入をしなければならない)
        this.name = name;
        this.age = age;
    }
}

カノニカルコンストラクタを自分で定義するときは、レコードが自動生成するコードを「上書き」するイメージです。自分でバリデーションや初期化処理を細かく書きたいときに使います。

コンパクトコンストラクタとバリデーション

レコードには、さらに便利な「コンパクトコンストラクタ」という書き方があります。

public record Person(String name, int age) {
    public Person {
        // コンパクトコンストラクタ
        // age が負数なら例外にするなど
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

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

レコードの活用例

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をコピーしました