概要
Java 17 以降では、データを保持する型として record・class・enum の3つの選択肢があります。enum は固定の定数セット、record は不変の値オブジェクト、class は可変状態やビジネスロジックを持つ汎用的な型、というのが大まかな棲み分けですが、実務では判断に迷う場面が少なくありません。注文ステータスは enum で十分か、注文明細は record にすべきか、ステータス遷移のロジックは class に置くのか。この記事では、受注処理を題材に3つの型を組み合わせた設計パターンを示しながら、それぞれの使いどころと限界を整理します。Java 8 環境で同じ設計をどう実現するかも併記するので、バージョン移行時の参考にもなるはずです。
使いどころ
注文ステータス(受付中・処理中・発送済みなど)を enum で定義し、注文データを record の DTO で表現する
マスタ区分値は enum、トランザクションデータは record、業務ロジックは class という責務分離を設計レビューで適用する
既存の POJO クラスを record に移行する際、可変状態を持つクラスとの境界線を見極める
コード例
import java.util.List;
public class RecordVsClassVsEnumDemo {
// === Enum: 固定の定数セット ===
enum OrderStatus {
PENDING("受付中"),
PROCESSING("処理中"),
SHIPPED("発送済み"),
DELIVERED("配達済み"),
CANCELLED("キャンセル");
private final String label;
OrderStatus(String label) { this.label = label; }
public String getLabel() { return label; }
}
// === Record: 不変の値オブジェクト(DTO) ===
record OrderDto(String orderId, String product,
int quantity, OrderStatus status) {
// with パターン: ステータスを変更した新しい DTO を返す
public OrderDto withStatus(OrderStatus newStatus) {
return new OrderDto(orderId, product, quantity, newStatus);
}
}
// === Class: 可変状態を持つビジネスロジック ===
static class OrderService {
private int processedCount = 0;
public OrderStatus advance(OrderStatus current) {
processedCount++;
if (current == OrderStatus.PENDING) return OrderStatus.PROCESSING;
if (current == OrderStatus.PROCESSING) return OrderStatus.SHIPPED;
return current;
}
public int getProcessedCount() { return processedCount; }
}
public static void main(String[] args) {
// Enum: ステータス一覧
System.out.println("=== Enum: 注文ステータス一覧 ===");
List.of(OrderStatus.values()).forEach(s ->
System.out.println(s.name() + " -> " + s.getLabel()));
// Class: ビジネスロジック
System.out.println("\n=== Class: ステータス遷移 ===");
var service = new OrderService();
var status = OrderStatus.PENDING;
status = service.advance(status); // PROCESSING
status = service.advance(status); // SHIPPED
System.out.println("現在: " + status.getLabel()
+ " (処理回数: " + service.getProcessedCount() + ")");
// Record: 不変 DTO の操作
System.out.println("\n=== Record: 注文 DTO ===");
var order = new OrderDto("ORD-001", "ノートPC", 2, OrderStatus.PENDING);
System.out.println(order);
var updated = order.withStatus(OrderStatus.PROCESSING);
System.out.println("更新後: " + updated);
System.out.println("元は不変: " + order.status().getLabel());
// 一覧表示
System.out.println("\n=== 注文一覧 ===");
var orders = List.of(
new OrderDto("ORD-001", "ノートPC", 2, OrderStatus.PENDING),
new OrderDto("ORD-002", "マウス", 5, OrderStatus.PROCESSING),
new OrderDto("ORD-003", "キーボード", 1, OrderStatus.SHIPPED)
);
orders.forEach(o ->
System.out.println(o.orderId() + ": " + o.product()
+ " -> " + o.status().getLabel()));
}
}Version Coverage
record が加わり、enum = 定数・record = 不変値・class = 可変状態/ロジック という三分類が明確になる。with パターンで不変更新も簡潔。
// Java 17: record で DTO を簡潔に定義
record OrderDto(String orderId, String product,
int quantity, OrderStatus status) {
public OrderDto withStatus(OrderStatus newStatus) {
return new OrderDto(orderId, product,
quantity, newStatus);
}
}
// enum OrderStatus { PENDING, PROCESSING, ... }
// class OrderService { OrderStatus advance(...) }Library Comparison
注意点
enum にビジネスロジックを詰め込みすぎると肥大化する。状態遷移のような振る舞いは別クラスに切り出す方が保守しやすい
record は継承できないため、共通フィールドを持つ複数の record を作る場合は interface でメソッドを共有する設計にする
record の withXxx パターンは便利だが、フィールド数が多い record では引数の順序ミスが起きやすい。ビルダーパターンの検討も視野に入れる
enum の values() は呼ぶたびに配列をコピーする。ループで大量に呼ぶ場合は List.of(values()) でキャッシュしておく
FAQ
enum にはラベル取得や変換のような自身の属性に閉じたメソッドを置き、状態遷移や業務判定のように外部依存があるロジックは別クラスに切り出すのが保守しやすい設計です。
フィールドが変更不要で、equals を全フィールドで比較して問題ないなら record を選びます。可変状態を持つ、equals をカスタムしたい、継承が必要、のいずれかに該当すれば class を使います。
enum は固定セットの表現に適しており、record の代わりにはなりません。逆にインスタンスごとに異なる値を持つデータは record が適しています。両者は補完関係であり、置き換えの関係ではありません。