前回は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(火) ぱんまる


コメント