【プログラミングしてみよう25】Javaのイミュータブルクラスとrecord

Java

前回はMapを使ってキーと値をセットで管理する方法を学びました。

今回は「イミュータブルクラス」と、Java 16から正式導入された「record」について解説します。 一度作ったら変更できないオブジェクトの設計方法を学ぶことで、バグを生みにくい安全なコードが書けるようになります。

イミュータブルクラスとは

「イミュータブル(immutable)」とは「変更不可能」という意味です。一度インスタンスを作ったら、その中身を変えられないクラスのことをイミュータブルクラスと呼びます。

String クラスはその代表例です。"hello" という文字列は変更できず、toUpperCase() などのメソッドは元の文字列を変えずに新しい文字列を返します。

イミュータブルクラスを作るには、以下のポイントを守ります。

  • すべてのフィールドを private final にする
  • セッター(setXxx)を用意しない
  • コンストラクタで値を設定し、ゲッター(getXxx)だけを用意する

イミュータブルにすることで、意図せず値が変更されるバグを防ぎやすくなり、マルチスレッド環境でも安全に共有できます。

サンプルコード(イミュータブルクラス)

ファイル作成

「Java14」フォルダに Point.javaImmutableTest.java という名前でファイルを作成します

コード入力

Point.java に以下のコードを入力します。

public final class Point {
    private final int x;
    private final int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public Point move(int dx, int dy) {
        return new Point(x + dx, y + dy);
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

ImmutableTest.java に以下のコードを入力します。

public class ImmutableTest {
    public static void main(String[] args) {
        Point p1 = new Point(3, 5);
        System.out.println("p1: " + p1);

        Point p2 = p1.move(2, -1);
        System.out.println("p2: " + p2);

        // p1は変わっていない
        System.out.println("p1はそのまま: " + p1);
    }
}

コンパイル

javac Point.java ImmutableTest.java

実行

java ImmutableTest

実行結果は次のようになります。

p1: Point(3, 5)
p2: Point(5, 4)
p1はそのまま: Point(3, 5)

recordの基本

イミュータブルなデータ保持クラスを毎回手書きするのは手間がかかります。private final フィールド・コンストラクタ・ゲッター・toString() など、決まり切った記述が多くなりがちです。

Java 16から正式導入された「record」を使うと、これらを自動的に生成してくれます。

public record レコード名(型 フィールド名, ...) { }

たったこれだけで、以下が自動的に用意されます。

  • private final フィールド
  • 全フィールドを引数に取るコンストラクタ
  • 各フィールドのゲッターメソッド
  • toString()equals()hashCode()

サンプルコード(record)

ファイル作成

「Java14」フォルダに Person.javaRecordTest.java という名前でファイルを作成します

コード入力

Person.java に以下のコードを入力します。

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

RecordTest.java に以下のコードを入力します。

import java.util.ArrayList;
import java.util.List;

public class RecordTest {
    public static void main(String[] args) {
        Person p1 = new Person("ぱんまる", 25);
        Person p2 = new Person("たろう", 30);
        Person p3 = new Person("ぱんまる", 25);

        // 自動生成されたtoStringを使って表示
        System.out.println(p1);
        System.out.println(p2);

        // ゲッターはフィールド名と同じメソッド名
        System.out.println("名前: " + p1.name());
        System.out.println("年齢: " + p1.age());

        // 自動生成されたequalsで内容比較
        System.out.println("p1とp3は同じ?: " + p1.equals(p3));
        System.out.println("p1とp2は同じ?: " + p1.equals(p2));

        // ListやMapとも組み合わせて使える
        List<Person> people = new ArrayList<>();
        people.add(p1);
        people.add(p2);
        people.forEach(p -> System.out.println(p.name() + "さん(" + p.age() + "歳)"));
    }
}

コンパイル

javac Person.java RecordTest.java

実行

java RecordTest

実行結果は次のようになります。

Person[name=ぱんまる, age=25]
Person[name=たろう, age=30]
名前: ぱんまる
年齢: 25
p1とp3は同じ?: true
p1とp2は同じ?: false
ぱんまるさん(25歳)
たろうさん(30歳)

コードの解説

イミュータブルクラスの Point では、フィールドに private final をつけてセッターを用意していないため、一度作ったインスタンスの xy を外から変えることができません。move() メソッドは自分自身を変更するのではなく、新しい Point インスタンスを作って返しています。クラス自体にも final をつけることで、継承によってイミュータブルが破られることも防いでいます。

toString()@Override で定義することで、System.out.println(p1) のように直接表示したときに Point(3, 5) という読みやすい形式で出力されます。

recordの Person では、たった1行 public record Person(String name, int age) { } と書くだけで、コンストラクタ・ゲッター・toString()equals()hashCode() がすべて自動生成されます。通常のクラスのゲッターは getName() のような形ですが、recordのゲッターはフィールド名そのものの name() という形になります。

equals() が自動生成されているため、p1.equals(p3) のように内容を比較でき、nameage が同じであれば true が返ります。以前学んだArrayListやラムダ式とも自然に組み合わせて使えます。

まとめ

イミュータブルクラスは private final フィールドとセッターなしのゲッターのみで設計し、値を変えたいときは新しいインスタンスを返すことで、意図しない変更を防ぎます。recordを使うと、イミュータブルなデータ保持クラスに必要なコードを大幅に省略できます。データを入れておくだけのシンプルなクラスを作りたいときは、まずrecordの使用を検討してみましょう。イミュータブルな設計はマルチスレッド環境でも安全で、バグを防ぎやすいコードにつながります。

2026/07/04(土) ぱんまる

コメント

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