クイックスタート:昔の苦労と新しい解決策(5分)
Javaをしばらく書いていると、お決まりの手順を知っているはずです。データクラスを作成し、コンストラクタを書き、ゲッターを書き、equals()をオーバーライドし、hashCode()をオーバーライドし、toString()をオーバーライドする。プロジェクト内のすべてのDTO、値オブジェクト、レスポンスモデルに対してこれを繰り返すのです。
以前のシンプルなPointクラスがどのようなものだったか見てみましょう:
// Java 8〜15:お決まりのボイラープレート地獄
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; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
@Override
public String toString() { return "Point[x=" + x + ", y=" + y + "]"; }
}
それをJava 16+と比べてみましょう:
public record Point(int x, int y) {}
たった1行。コンパイラが標準コンストラクタ、アクセサ(x()とy() — getX()ではない)、そして正しいequals、hashCode、toStringを自動生成します。recordは暗黙的にfinalかつイミュータブルです。
この一つの変更で、クラスごとに30〜40行が削減されます。20以上のDTOを持つ典型的なSpring Bootプロジェクトでは、600〜800行のボイラープレートが丸ごと消えます。そのコードを読む際の認知的負荷は本物です — 取り除くことでコードレビューが再び意味のあるものになります。
詳細解説:各機能の理解
Records — シンプルなDTO以上の存在
Recordsは単なるシンタックスシュガーではありません。「この型はそのコンポーネントの透明でイミュータブルなキャリアである」という契約をエンコードしています。この意味的な明確さは、インシデント対応中の深夜2時に見慣れないコードを読む際に非常に重要です。
コンパクトコンストラクタでバリデーションを追加できます:
public record Range(int min, int max) {
// コンパクトコンストラクタ:パラメータリストを繰り返す必要なし
Range {
if (min > max) throw new IllegalArgumentException(
"min %d は max %d 以下でなければなりません".formatted(min, max)
);
}
}
バリデーション以外にも、recordsはインターフェースを実装し、staticや追加のインスタンスメソッドを定義できます。一つの明確な制限:宣言されたコンポーネント以外に新しいインスタンスフィールドを持てません。
public interface Measurable {
double length();
}
public record Segment(Point start, Point end) implements Measurable {
@Override
public double length() {
int dx = end.x() - start.x();
int dy = end.y() - start.y();
return Math.sqrt(dx * dx + dy * dy);
}
}
Sealed Classes — 制御された型ヒエラルキー
オープンな継承には根本的な問題があります:存在するすべてのサブクラスを列挙することができません。Shapeを受け取るメソッドを書いても、誰かどこかで、switchステートメントがカバーしていないHexagonサブクラスを追加していないという保証はありません。
Sealed classesは言語レベルでこの問題を解決します:
// Circle、Rectangle、Triangleのみが許可されている
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
permits句は明示的でコンパイル時に検証されます。許可された各サブクラスはfinal、sealed(さらに制限される)、またはnon-sealed(意図的に再開放)でなければなりません。
RustのenumやHaskellの直和型を使ったことがあれば、これはJavaの同等物です — 代数的データ型が、ついにJVMに登場しました。
パターンマッチング — 意味をなすSwitch式
パターンマッチングは段階的に導入されました。instanceofバージョンはJava 16で安定版になりました:
// 旧来の方法
if (obj instanceof String) {
String s = (String) obj; // 冗長なキャスト
System.out.println(s.length());
}
// Java 16+のパターン変数
if (obj instanceof String s) {
System.out.println(s.length()); // sはすでに型付けされている
}
Java 21 LTSでは完全なswitchパターンマッチングが導入され、sealed classesと完璧に組み合わせられます:
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// defaultは不要 — コンパイラがこれらがすべてのケースだと知っている
};
}
defaultケースは不要です。switchを更新せずにsealed interfaceにPentagonを追加すると、コンパイラがビルドを拒否します。コンパイル時に検出される — オンコールエンジニアがすでにストレスを抱えている深夜3時ではなく。
応用編:3つの機能の組み合わせ
3つを組み合わせるところが本当に面白くなります。シンプルな式評価器でそのパターンを明確に示します:
// recordsを使ったsealedヒエラルキー
public sealed interface Expr
permits Literal, Add, Multiply, Negate {}
public record Literal(double value) implements Expr {}
public record Add(Expr left, Expr right) implements Expr {}
public record Multiply(Expr left, Expr right) implements Expr {}
public record Negate(Expr expr) implements Expr {}
// 評価のためのパターンマッチング — 網羅的で読みやすい
static double eval(Expr expr) {
return switch (expr) {
case Literal(var v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Multiply(var l, var r) -> eval(l) * eval(r);
case Negate(var e) -> -eval(e);
};
}
Literal(var v)の構文はJava 21で利用可能なrecordパターン分解です。caseラベルでrecordのコンポーネントを直接バインドします — ブランチ内でexpr.value()を呼び出す必要はありません。
より細かい制御が必要ですか?whenでガードを追加できます:
String describe(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "大きな円";
case Circle c -> "小さな円";
case Rectangle r when r.width() == r.height() -> "正方形";
case Rectangle r -> "長方形";
case Triangle t -> "三角形";
};
}
switchは上から下に評価され、最初に一致したcaseが勝ちます。ネストされたif文も、フォールスルーの驚きもありません。
実際のプロジェクトのための実践的なヒント
APIレスポンスモデルにRecordsを使う
Jackson 2.12+はrecordsをそのまま扱えます — JSONフィールドをコンストラクタパラメータに直接マッピングします。APIペイロードに不要なフィールドが含まれる場合は@JsonIgnoreProperties(ignoreUnknown = true)を追加します:
public record UserResponse(
long id,
String username,
String email
) {}
@JsonPropertyもemptyコンストラクタもLombokも不要です。デフォルトでクリーンかつイミュータブルです。
ドメインイベントをSealする
イベントソーシングとCQRSパターンはsealed interfacesから大きな恩恵を受けます。permits句にすべてのイベントタイプを列挙すれば、ハンドラが網羅的になります — コンパイラが本番環境に到達する前に漏れているケースを検出します:
public sealed interface OrderEvent
permits OrderPlaced, OrderShipped, OrderCancelled {}
public record OrderPlaced(String orderId, List<String> items) implements OrderEvent {}
public record OrderShipped(String orderId, String trackingNumber) implements OrderEvent {}
public record OrderCancelled(String orderId, String reason) implements OrderEvent {}
void handleEvent(OrderEvent event) {
switch (event) {
case OrderPlaced e -> processNewOrder(e.orderId(), e.items());
case OrderShipped e -> notifyCustomer(e.orderId(), e.trackingNumber());
case OrderCancelled e -> refundOrder(e.orderId(), e.reason());
}
}
採用前にJavaバージョンを確認する
- Java 16:Records安定版、
instanceofパターン安定版 - Java 17 LTS:Sealed classes安定版 — これらの機能の最小推奨バージョン
- Java 21 LTS:Switchパターンマッチング安定版、recordデコンストラクション安定版 — 現在の推奨ターゲット
まだJava 11を使っていますか?今すぐアップグレードの計画を始めましょう。Java 17 LTSは2029年まで延長サポートがあり、Java 21 LTSは2031年まで続きます。どちらもあなたを困らせることはなく、この記事で取り上げたすべての機能が利用できます。
Recordsを無理にどこでも使わない
RecordsはデータキャリアとしてDTO、値オブジェクト、レスポンスモデル、設定スナップショットに最適です。JPAエンティティ(可変状態、引数なしコンストラクタが必要)や複雑な継承を中心に構築されたクラスには不向きです。適切なツールを使いましょう。
Records、Sealed Classes、パターンマッチングを組み合わせることで、ドメインのルールを型システムにエンコードできます — 無効な状態が本番環境に紛れ込む代わりに、コンパイル時にエラーとして検出されます。この保証はコードベースが成熟してから後付けするのは困難です。最初から取り入れる価値があります。

