概要
業務システムの開発では、API レスポンスの格納先や画面表示用のデータ転送など、フィールドを持つだけの不変クラスを作る場面が頻繁にあります。従来の Java ではそのたびに equals・hashCode・toString・getter を手書きし、フィールドを追加するたびにこれらを修正する必要がありました。Java 16 で正式導入された record は、この定型コードをコンパイラが自動生成してくれる仕組みです。この記事では、record の基本構文とコンパクトコンストラクタによるバリデーション、メソッドの追加、DTO としての設計パターンを、動くコードで整理します。Java 8 環境で同等のクラスを書く場合との比較も示すので、移行の判断材料にもなるはずです。
使いどころ
REST API のレスポンスを UserDto record で受け取り、画面表示やログ出力に toString を活用する
CSV 取込時の1行分のデータを record で表現し、コンパクトコンストラクタで必須項目のバリデーションを行う
帳票出力用の集計結果を record に詰めて返却し、呼び出し元での意図しない書き換えを防ぐ
コード例
import java.util.List;
public class RecordBasicDemo {
// record で DTO を定義(toString / equals / hashCode 自動生成)
record Person(String name, int age) {}
// コンパクトコンストラクタでバリデーション
record ValidatedAge(int value) {
ValidatedAge {
if (value < 0 || value > 150) {
throw new IllegalArgumentException(
"年齢は 0〜150 の範囲で指定してください: " + value);
}
}
}
// record にメソッドを追加できる
record Rectangle(double width, double height) {
public double area() { return width * height; }
// with パターン: フィールドを変更した新しいインスタンスを返す
public Rectangle withWidth(double newWidth) {
return new Rectangle(newWidth, height);
}
}
// 業務 DTO の例
record UserDto(int id, String email, String displayName) {}
public static void main(String[] args) {
// 基本操作: 生成・toString・equals
var p1 = new Person("田中太郎", 25);
var p2 = new Person("田中太郎", 25);
System.out.println(p1); // Person[name=田中太郎, age=25]
System.out.println("equals: " + p1.equals(p2)); // true
// コンパクトコンストラクタによるバリデーション
try {
new ValidatedAge(-1);
} catch (IllegalArgumentException e) {
System.out.println("検証エラー: " + e.getMessage());
}
// メソッド追加と with パターン
var rect = new Rectangle(5.0, 3.0);
System.out.println("面積: " + rect.area());
var wider = rect.withWidth(10.0);
System.out.println("幅変更後: " + wider);
// DTO としての一覧操作
var users = List.of(
new UserDto(1, "[email protected]", "田中太郎"),
new UserDto(2, "[email protected]", "山田花子")
);
users.forEach(System.out::println);
}
}Version Coverage
record が正式に使える。コンパクトコンストラクタでバリデーションも簡潔に書ける。toString の出力形式は自動で className[field=value] になる。
// Java 17: record で同じ機能を1行で定義
record Person(String name, int age) {}
// toString, equals, hashCode, アクセサが自動生成される
// コンパクトコンストラクタでバリデーション
record ValidatedAge(int value) {
ValidatedAge {
if (value < 0 || value > 150) {
throw new IllegalArgumentException(
"年齢は 0〜150: " + value);
}
}
}Library Comparison
注意点
record は暗黙的に final であり、継承できない。既存の class 階層に組み込みたい場合は interface の implements を使う
コンパクトコンストラクタではフィールドへの代入を書かない。代入はコンパイラが末尾に自動挿入するため、明示すると重複代入のコンパイルエラーになる
record のフィールドはすべて final なので、値を変えたい場合は withXxx メソッドで新しいインスタンスを返すパターンを使う
record の equals は全フィールドの値比較を行う。大量の record インスタンスを HashSet や HashMap のキーにする場合、hashCode の衝突率にも注意が必要
record に static フィールドは追加できるが、インスタンスフィールドを追加することはできない。コンポーネントとして宣言したもの以外は持てない設計になっている
FAQ
record のアクセサは name() のように get なしが標準です。JavaBeans 規約が求められるフレームワーク(一部の JSP・EL 式など)では別途 getName() を定義する必要がありますが、それ以外では record 標準のアクセサを使うのが自然です。
withXxx メソッドを自前で定義し、新しい record を返す方法が一般的です。例えば withAge(int newAge) で new Person(name, newAge) を返します。Java 標準にはまだ with 構文はありません。
record は暗黙的に final なので、サブクラスがある場合は移行できません。また equals の比較ロジックが全フィールド比較に変わるため、既存コードで一部フィールドだけで比較していた場合は動作が変わります。