0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java17/21は設計をどこまで単純にしたのか ~RecordとSealedがAI生成コードにもたらした本当の変化~

0
Last updated at Posted at 2026-04-09

本記事は、2026/5/30開催の「JJUG CCC 2026 Spring」の登壇内容に関連した技術記事シリーズです。
AIエージェント(Copilot / Claude Code / Cursor)の普及により、Java開発の前提は大きく変わりました。
本シリーズでは「AI時代における設計の変化」をテーマに、実務視点で整理していきます。

【シリーズ一覧】

  1. AI時代にJava設計はどう変わったのか
  2. カッペリーニコードとは何か
  3. Java17/21は設計をどこまで単純化したのか
  4. AIは設計できるのか
  5. AI時代の設計ガードレール

はじめに

Java 17 / 21 は、実装の複雑さを大幅に削減した。
RecordはDTOのボイラープレートを消し、Sealed Classesは型階層を閉じ、
Pattern Matchingは冗長なキャストを不要にした。

しかし、「実装がシンプルになった」と「設計が単純化した」は別の話だ。

AIがこれらの構文を使いこなせるようになった今、本質的な問いが生まれる。

言語進化はAI生成コードの設計品質を上げたのか。
それとも設計崩壊をより洗練された形で隠蔽しただけなのか。

Spring Bootの実務視点から、Java17/21の主要機能を設計観点で再評価する。

機能の設計的な位置づけ

各機能は「書きやすくする機能」ではなく、
「設計意図を型で表現する機能」 だという認識が出発点になる。

機能 導入 設計上の役割
Record Java 16 不変データ構造の型レベル表明
Sealed Classes Java 17 型階層の閉じた表現
Pattern Matching for switch Java 21 型依存ロジックの構造化と網羅性保証
Virtual Threads Java 21 並行処理モデルの刷新

Record

ボイラープレートの消滅

Spring BootのAPIでは、DTOクラスが大量に登場する。
従来はこれだけのコードが必要だった。

public class UserResponse {
    private final String id;
    private final String name;

    public UserResponse(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() { return id; }
    public String getName() { return name; }

    @Override public boolean equals(Object o) { ... }
    @Override public int hashCode() { ... }
    @Override public String toString() { ... }
}

Recordを使えばこうなる。

public record UserResponse(String id, String name) {}

constructor / getter / equals / hashCode / toString はすべて自動生成される。
これは記法の省略ではなく、
「このクラスは不変のデータの集合である」という設計意図の型レベルでの表明だ。

Spring Bootでの実務的な使い所

APIレスポンスとRecordの相性は良い。

@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public UserResponse getUser(@PathVariable String id) {
        return new UserResponse(id, "Taro");
    }
}

Recordはimmutableであるため、スレッドセーフで状態変更バグが起きない。
現在のSpring Bootアプリケーションでは DTO = Record という設計も珍しくない。

用途は主に以下の4つだ。

  • Request / Response DTO
  • Projectionオブジェクト
  • ドメインの値オブジェクト(Value Object)
  • Compact Constructorを使った自己検証型

Compact Constructorで検証を型に組み込む例がこれだ。

public record Money(BigDecimal amount, Currency currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金額は0以上が必要です");
        }
        Objects.requireNonNull(currency, "通貨は必須です");
    }
}

不正な状態のオブジェクトが構築できなくなる。
「バリデーションはServiceで行う」という習慣を見直すきっかけになる構文だ。

AIとRecordの落とし穴

AIはRecordを積極的に使う。しかし次のようなコードを生成しがちだ。

// AIがよく生成するパターン(問題あり)
public record OrderRequest(
    String userId,
    List<String> itemIds,
    String deliveryAddress,
    String paymentMethod,
    String couponCode,
    boolean isGift,
    String giftMessage
) {}

フィールドが多すぎるRecordは、概念の境界が設計されていないサインだ。

OrderRequest は本来 DeliveryInfoPaymentInfoGiftOption などに分割されるべきかもしれない。
Recordがコンパクトに書けるからこそ、AIはフィールドを詰め込みがちになる。

Recordの採用は設計の終点ではなく、概念を正しく切り出した結果として選ぶものだ。

Sealed Classes

型階層を「閉じる」設計

従来の継承は無制限だった。

// 従来:誰でも継承できてしまう
public abstract class PaymentStatus {}

Sealed Classesで継承可能な型を明示的に制限できる。

public sealed interface PaymentStatus
        permits Success, Failed, Pending {}

public record Success(String transactionId) implements PaymentStatus {}
public record Failed(String errorCode, String message) implements PaymentStatus {}
public record Pending(String referenceId) implements PaymentStatus {}

「この業務ルールにおいてあり得る状態はこれだけだ」という設計意図が型に現れる。

Pattern Matching for switchとの組み合わせ

Java 21のswitch式と組み合わせると、コンパイラが網羅性を保証する

public String buildMessage(PaymentStatus status) {
    return switch (status) {
        case Success s   -> "支払い完了: " + s.transactionId();
        case Failed f    -> "支払い失敗: " + f.message();
        case Pending p   -> "処理中: " + p.referenceId();
    };
}

PaymentStatus に新しいサブタイプを追加すると、このswitch文はコンパイルエラーになる。
型の追加が即座に処理漏れを検出する設計になっている。

これはドメイン状態モデルに特に有効だ。

public sealed interface OrderState
        permits Created, Paid, Shipped, Cancelled {}

未定義状態・想定外状態をコンパイル時に排除できる。
状態遷移の安全性を、ランタイムではなくコンパイル時に担保する。

AIはSealedをどう扱うか

AIはSealedの構文を正しく生成できる。
しかし 「どこをSealedにすべきか」という判断はできない。

よくあるパターンはこれだ。

// 汎用型で済ませてしまうケース
public sealed interface ApiResponse<T>
        permits ApiResponse.Success, ApiResponse.Error {}

public record Success<T>(T data) implements ApiResponse<T> {}
public record Error<T>(String message) implements ApiResponse<T> {}

PaymentResponseOrderResponse それぞれで意味のある状態を持つべき場面で、
汎用型で済ませることでドメイン固有のバリアントが型に現れなくなる。

Sealedは「閉じた型階層」の構文ではなく、
業務ルールの境界を型で表明するための道具だ。

Record + Sealed + Pattern Matchingが揃うと何が起きるか

3つを組み合わせた設計は、代数的データ型(ADT)に近い表現をJavaで実現する。

// 注文の状態遷移を型で表現
public sealed interface OrderStatus
        permits Pending, Confirmed, Shipped, Cancelled {}

public record Pending(LocalDateTime createdAt) implements OrderStatus {}
public record Confirmed(LocalDateTime confirmedAt, String warehouseId) implements OrderStatus {}
public record Shipped(LocalDateTime shippedAt, String trackingNumber) implements OrderStatus {}
public record Cancelled(LocalDateTime cancelledAt, String reason) implements OrderStatus {}

// 処理側:コンパイラが網羅性を保証
public BigDecimal calculateShippingFee(OrderStatus status) {
    return switch (status) {
        case Pending p    -> BigDecimal.ZERO;
        case Confirmed c  -> fetchFeeByWarehouse(c.warehouseId());
        case Shipped s    -> BigDecimal.ZERO;
        case Cancelled c  -> BigDecimal.ZERO;
    };
}

各状態に必要なデータだけが型に含まれ、存在しえない状態が型レベルで排除される。
「コードが短い」のではなく、設計の意図が型システムに乗っているのがこの構造の本質だ。

Virtual Thread

スレッドモデルの変化

従来のSpring Bootアプリケーションのモデルはこうだった。

Request → Platform Thread → DB / I/O → Response

プラットフォームスレッドはOSスレッドに1:1対応するため重く、
高負荷なアプリケーションではThread Pool管理・非同期処理・Reactiveといった設計が必要だった。

Java 21のVirtual Threadは極めて軽量で、数十万スレッドを並列に扱える。

Spring Boot 3.2以降では設定1行で有効化できる。

spring.threads.virtual.enabled=true

Reactiveを使わずに、同期コードのまま高いスケーラビリティを実現できる。

設計への影響

Virtual Threadは「Reactiveを書かなくていい」だけではない。

設計観点での影響は次の通りだ。

  • Blocking I/Oを恐れなくていい:JDBC・RestTemplateの同期呼び出しが許容しやすくなる
  • Reactive設計の必要性が下がる:WebFlux前提だったユースケースが同期コードで書けるようになる
  • ただし、CPUバウンドな処理には効果がない:I/Oバウンドな処理に限定した効能だ

Virtual Threadは「設計を簡単にした」のではなく、
「Reactiveという設計の複雑さを回避できる選択肢を増やした」 のが正確な評価だ。

何が良くなり、何が残ったか

言語進化で改善されたこと

  • ボイラープレートが消えた分、AIが生成するコードの見た目の品質は上がった
  • Sealed + switchの組み合わせで処理漏れをコンパイル時に検出できる設計をAIが生成できるようになった
  • Recordにより不変性の表明がデフォルトになった

設計の複雑さが移動した場所

言語は実装の複雑さを削減した。しかし設計の難しさは消えたのではなく、別の場所に移動した。

削減された複雑さ 移動先の複雑さ
DTOのボイラープレート 概念をどう切り出すかの判断(Recordの粒度)
型判定の冗長なキャスト どこをSealedにするかの業務分析
Reactiveの複雑な記述 Virtual Threadでも残るトランザクション設計
スレッド管理コード マイクロサービス・分散システムの設計複雑性

AIはこれらの削減された複雑さを担当する
しかし移動した複雑さを扱うのは依然として人間の仕事だ。

まとめ

Java 17/21がもたらしたのは、設計の意図をコードで表現するための語彙の拡張だ。

  • Recordは「不変のデータ集合」を型で表明する語彙
  • Sealed Classesは「閉じた型階層」を型で表明する語彙
  • Pattern Matchingは「型に基づく処理分岐」を明示し、網羅性を保証する語彙
  • Virtual Threadは「Reactiveを使わない」という設計判断の選択肢

AIはこれらの語彙を流暢に使いこなす。
しかしどの語彙を、いつ、なぜ使うかは設計の判断であり、AIはそこに踏み込まない。

言語が進化してコードがシンプルになったからこそ、
「なぜそう設計したか」がより鋭く問われるようになった。

次回(第4回)では「AIは本当に設計できるのか」を正面から問う。
AIが担える設計と、担えない設計の境界線を実例とともに整理する。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?