java-recipes

ホーム Record › Rec-01

Rec-01: Record の基本

Java 16 で正式導入された record は、 データを保持するためのクラス(DTO・値オブジェクト)を簡潔に書くための仕組みです。toStringequalshashCode を自動生成します。

record とは何か

API のレスポンスや設定値など「値を保持するためだけのクラス」を作るとき、 従来は final フィールド・コンストラクタ・ゲッター・toStringequalshashCode をすべて手書きする必要がありました。 record を使うと1行の宣言でこれらをすべて自動生成できます。

record が自動生成するもの

  • コンポーネントアクセサ: name()age() のようなゲッターメソッド(get プレフィックスなし)
  • コンストラクタ: すべてのフィールドを引数に取る標準コンストラクタ
  • toString(): Person[name=田中太郎, age=25] 形式
  • equals(): すべてのフィールドの値が同じなら等価と判定
  • hashCode(): すべてのフィールドをもとにハッシュ値を生成

record の制約

  • フィールドは暗黙的に final — 作成後に値を変更できません(イミュータブル)
  • 他のクラスを継承できません(暗黙的に final クラス)
  • インターフェースは実装できます
  • メソッドの追加は可能です

サンプルコード

RecordBasicSample.java
import java.util.Objects;

public class RecordBasicSample {

    // Java 8 では record を使えないので、手動で等価クラスを作成
    // record Person(String name, int age) と同等の実装

    static final class Person {
        private final String name; // ✅ final フィールド(不変性)
        private final int age;

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

        // record は以下を自動生成する
        public String name() { return name; } // ゲッターは get なし
        public int age() { return age; }

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

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

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

    // DTO 例: APIレスポンスの表現
    static final class UserDto {
        private final int id;
        private final String email;
        private final String displayName;

        public UserDto(int id, String email, String displayName) {
            this.id = id;
            this.email = email;
            this.displayName = displayName;
        }

        public int id() { return id; }
        public String email() { return email; }
        public String displayName() { return displayName; }

        @Override
        public String toString() {
            return "UserDto[id=" + id + ", email=" + email + ", displayName=" + displayName + "]";
        }

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof UserDto)) return false;
            UserDto u = (UserDto) o;
            return id == u.id && Objects.equals(email, u.email);
        }

        @Override
        public int hashCode() { return Objects.hash(id, email); }
    }

    public static void main(String[] args) {
        System.out.println("=== Person クラス(record の Java 8 相当) ===");
        Person p1 = new Person("田中太郎", 25);
        Person p2 = new Person("田中太郎", 25);
        Person p3 = new Person("山田花子", 30);

        System.out.println(p1);
        System.out.println("p1.equals(p2): " + p1.equals(p2)); // true(同じ値)
        System.out.println("p1.equals(p3): " + p1.equals(p3)); // false
        System.out.println("p1 == p2: " + (p1 == p2)); // false(別インスタンス)

        System.out.println("\n=== UserDto ===");
        UserDto user = new UserDto(1, "taro@example.com", "田中太郎");
        System.out.println(user);
    }
}

Java 8 では record が使えないため、toString / equals / hashCode をすべて手書きで実装する必要があります。Java 16+ に移行すると record の1行宣言でこれらをすべて自動生成できます。

よくあるミス・注意点

⚠️ record のフィールドは変更不可(セッターは存在しない)

record のフィールドは暗黙的に final です。 作成後に値を変更する setName() のようなメソッドを追加しようとしてもコンパイルエラーになります。 値を変更したい場合は新しい record インスタンスを作成してください。 例えば名前だけ変えたい場合は new Person("別の名前", person.age()) のようにします。

⚠️ record は継承できない(final 扱い)

record は暗黙的に final クラスとして扱われるため、 他のクラスが record を継承することはできません。また、record 自身も他のクラスを継承できません(java.lang.Record を除く)。 共通の振る舞いを持たせたい場合は、インターフェースを実装する方法を使いましょう。

テストする観点

  • 同じ値を持つ record 同士が equals() で等価(true)であること
  • 異なる値を持つ record が equals() で非等価(false)であること
  • 同じ値の record が同じ hashCode() を返すこと(HashMap のキーとして使える)
  • コンパクトコンストラクタのバリデーションが正しく動作すること(境界値: 年齢 0、150 は正常、-1 と 151 は例外)
  • toString()Person[name=..., age=...] 形式で出力されること
  • record 内のメソッド(area()・perimeter())が正しく計算されること

GitHub でソースコードを見る →