【プログラミングしてみよう56】Javaのrecordを深める(コンパクトコンストラクタ・カスタムメソッド)

Java

前回はOptionalとStreamを組み合わせた安全なデータ処理を学びました。

今回は「record」をさらに深掘りします。 以前はrecordの基本(自動生成されるコンストラクタ・ゲッター・equals・toString)を学びましたが、今回はバリデーションを追加できる「コンパクトコンストラクタ」やカスタムメソッドの定義など、より実践的な使い方を解説します。

recordの復習と今回の内容

以前学んだrecordの基本はこうでした。

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

たった1行で name()・age()・equals()・hashCode()・toString() が自動生成されます。今回はこれに加えて以下を学びます。

コンパクトコンストラクタ // バリデーションや値の正規化ができる
カスタムメソッド        // recordに独自のメソッドを追加できる
インターフェースの実装  // recordもインターフェースをimplementsできる
with風メソッド          // 一部の値だけ変えた新しいrecordを返す

サンプルコード(コンパクトコンストラクタでバリデーション)

ファイル作成

「Java44」フォルダに Email.java、Range.java、RecordValidTest.java という名前でファイルを作成します

コード入力

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

public record Email(String value) {
    // コンパクトコンストラクタ: {}の中にバリデーションを書く
    public Email {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("メールアドレスは空にできません");
        }
        if (!value.contains("@")) {
            throw new IllegalArgumentException("メールアドレスの形式が正しくありません: " + value);
        }
        // 値の正規化(小文字に統一)
        value = value.toLowerCase().trim();
    }

    // カスタムメソッド
    public String domain() {
        return value.substring(value.indexOf("@") + 1);
    }

    public String localPart() {
        return value.substring(0, value.indexOf("@"));
    }
}

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

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException(
                "minはmax以下でなければなりません: min=" + min + ", max=" + max);
        }
    }

    public boolean contains(int value) {
        return value >= min && value <= max;
    }

    public int length() {
        return max - min;
    }

    // 一部の値だけ変えた新しいrecordを返す(with風メソッド)
    public Range withMin(int newMin) {
        return new Range(newMin, max);
    }

    public Range withMax(int newMax) {
        return new Range(min, newMax);
    }
}

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

public class RecordValidTest {
    public static void main(String[] args) {
        // Emailのバリデーション
        System.out.println("--- Email ---");
        try {
            Email email1 = new Email("Panmaru@Example.COM");
            System.out.println("email1: " + email1.value());
            System.out.println("ドメイン: " + email1.domain());
            System.out.println("ローカル部: " + email1.localPart());
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }

        try {
            Email invalid = new Email("not-an-email");
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }

        // Rangeのバリデーションとカスタムメソッド
        System.out.println("\n--- Range ---");
        Range range = new Range(1, 10);
        System.out.println("range: " + range);
        System.out.println("5は含まれる?: " + range.contains(5));
        System.out.println("15は含まれる?: " + range.contains(15));
        System.out.println("長さ: " + range.length());

        Range extended = range.withMax(20);
        System.out.println("拡張後: " + extended);

        try {
            Range invalid = new Range(10, 1); // min > max
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

コンパイル

javac Email.java Range.java RecordValidTest.java

実行

java RecordValidTest

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

--- Email ---
email1: panmaru@example.com
ドメイン: example.com
ローカル部: panmaru
エラー: メールアドレスの形式が正しくありません: not-an-email

--- Range ---
range: Range[min=1, max=10]
5は含まれる?: true
15は含まれる?: false
長さ: 9
拡張後: Range[min=1, max=20]
エラー: minはmax以下でなければなりません: min=10, max=1

サンプルコード(カスタムメソッドとインターフェースの実装)

ファイル作成

「Java44」フォルダに Shape.java、Circle.java、Rectangle.java、RecordInterfaceTest.java という名前でファイルを作成します

コード入力

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

public interface Shape {
    double area();
    double perimeter();
    default String describe() {
        return String.format("%s: 面積=%.2f, 周囲=%.2f",
                getClass().getSimpleName(), area(), perimeter());
    }
}

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

public record Circle(double radius) implements Shape {
    public Circle {
        if (radius <= 0) {
            throw new IllegalArgumentException("半径は正の値でなければなりません");
        }
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }

    @Override
    public double perimeter() {
        return 2 * Math.PI * radius;
    }
}

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

public record Rectangle(double width, double height) implements Shape {
    public Rectangle {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("幅と高さは正の値でなければなりません");
        }
    }

    @Override
    public double area() {
        return width * height;
    }

    @Override
    public double perimeter() {
        return 2 * (width + height);
    }

    public boolean isSquare() {
        return width == height;
    }
}

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

import java.util.List;
import java.util.Comparator;

public class RecordInterfaceTest {
    public static void main(String[] args) {
        var shapes = List.of(
            new Circle(5),
            new Rectangle(4, 6),
            new Rectangle(3, 3),
            new Circle(2)
        );

        // describe()でそれぞれの情報を表示
        System.out.println("--- 図形一覧 ---");
        shapes.forEach(s -> System.out.println(s.describe()));

        // 面積の大きい順にソート
        System.out.println("\n--- 面積順 ---");
        shapes.stream()
              .sorted(Comparator.comparingDouble(Shape::area).reversed())
              .forEach(s -> System.out.printf("%s: %.2f%n",
                      s.getClass().getSimpleName(), s.area()));

        // 正方形かどうか判定
        System.out.println("\n--- 正方形チェック ---");
        shapes.stream()
              .filter(s -> s instanceof Rectangle)
              .map(s -> (Rectangle) s)
              .forEach(r -> System.out.println(r + " は正方形?: " + r.isSquare()));

        // バリデーションエラー
        System.out.println("\n--- バリデーション ---");
        try {
            new Circle(-1);
        } catch (IllegalArgumentException e) {
            System.out.println("エラー: " + e.getMessage());
        }
    }
}

コンパイル

javac Shape.java Circle.java Rectangle.java RecordInterfaceTest.java

実行

java RecordInterfaceTest

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

--- 図形一覧 ---
Circle: 面積=78.54, 周囲=31.42
Rectangle: 面積=24.00, 周囲=20.00
Rectangle: 面積=9.00, 周囲=12.00
Circle: 面積=12.57, 周囲=12.57

--- 面積順 ---
Circle: 78.54
Rectangle: 24.00
Circle: 12.57
Rectangle: 9.00

--- 正方形チェック ---
Rectangle[width=4.0, height=6.0] は正方形?: false
Rectangle[width=3.0, height=3.0] は正方形?: true

--- バリデーション ---
エラー: 半径は正の値でなければなりません

コードの解説

「コンパクトコンストラクタ」はrecordの特別なコンストラクタで、引数リストを省略して public レコード名 { } と書きます。中にバリデーションや値の正規化を書けます。Email の例では受け取った value を toLowerCase().trim() で小文字・空白除去に変換しています。このようにコンパクトコンストラクタの中でコンポーネント変数に再代入することで、格納される値を正規化できます。

Range の withMin や withMax はrecordのイミュータブルな性質を活かした「with風メソッド」です。自分自身は変更せず、一部の値だけ変えた新しいrecordを返します。Java 21以降では with 式として言語レベルでサポートされる予定です。

recordは implements でインターフェースを実装できます。Circle と Rectangle が Shape インターフェースを実装することで、前回学んだポリモーフィズムがrecordでも活用できます。コンパクトコンストラクタでバリデーションを入れることで、不正な値のオブジェクトが生成されることを防ぎます。

まとめ

recordのコンパクトコンストラクタを使うことで、自動生成されるコンストラクタにバリデーションや値の正規化を追加できます。またrecordにカスタムメソッドを定義したりインターフェースを実装したりすることで、シンプルさを保ちながらより豊かな振る舞いを持たせられます。with風メソッドでイミュータブルな性質を活かしたデータ変換も実現できます。recordは「シンプルなデータ保持クラス」という印象が強いですが、これらの機能を使うことでより実践的な場面でも活躍します。

2026/08/04(火) ぱんまる

コメント

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