はじめに

Javaにおける継承は、コードの再利用と拡張性を実現する強力な機能です。しかし、従来のJavaでは継承を「完全に禁止(final)」するか「完全に許可」するかの二択しかありませんでした。

Sealed Classes(シールドクラス) は、Java 17で正式導入された機能で、「どのクラスが継承できるか」を明示的に制御できます。これにより、クラス階層を厳密に管理し、より安全で予測可能な型設計が可能になります。

この記事では、Sealed Classesの基本概念から、permits句による継承先の制限、継承先の3つの選択肢(finalsealednon-sealed)、Sealed Interfaces、パターンマッチングとの連携、代数的データ型の表現まで、実践的なコード例とともに解説します。

Sealed Classesとは何か

Sealed Classesは、クラスまたはインターフェースを継承・実装できるクラスを明示的に制限する機能です。sealed修飾子とpermits句を使用して、許可されたサブクラスを宣言します。

Sealed Classesの導入経緯

Sealed Classesは以下の経緯を経て正式導入されました。

バージョン ステータス JEP
Java 15 プレビュー機能(第1弾) JEP 360
Java 16 プレビュー機能(第2弾) JEP 397
Java 17 正式機能 JEP 409

なぜSealed Classesが必要なのか

従来のJavaでは、継承の制御に以下の制限がありました。

アプローチ 問題点
finalクラス サブクラスを一切作れない(制限が強すぎる)
パッケージプライベート 同一パッケージ内でしか見えない(公開APIに不向き)
public abstractクラス 誰でも継承できる(制限がなさすぎる)

Sealed Classesは、これらの中間に位置し、「公開しつつも継承先を限定する」という要件を満たします。

Sealed Classesの概念図

flowchart TB
    subgraph "従来のJava"
        direction TB
        A1["finalクラス"] --> A2["継承不可"]
        B1["publicクラス"] --> B2["誰でも継承可能"]
    end
    
    subgraph "Sealed Classes"
        direction TB
        C1["sealed class Shape"] --> C2["permits句で指定した\nクラスのみ継承可能"]
        C2 --> D1["Circle"]
        C2 --> D2["Rectangle"]
        C2 --> D3["Triangle"]
    end

基本的な構文

Sealed Classesの基本構文は以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 継承を許可するクラスをpermits句で明示
public sealed class Shape permits Circle, Rectangle, Triangle {
    // クラス本体
}

// 許可されたサブクラスはfinal、sealed、non-sealedのいずれかで宣言
public final class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    public double getRadius() {
        return radius;
    }
}

public final class Rectangle extends Shape {
    private double width;
    private double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
}

public final class Triangle extends Shape {
    private double base;
    private double height;
    
    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
}

実行環境: Java 17以降

sealedとpermits句の使い方

permits句の基本

permits句は、sealedクラスを継承できるクラスを明示的に列挙します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public sealed class Vehicle permits Car, Motorcycle, Truck {
    protected String brand;
    
    public Vehicle(String brand) {
        this.brand = brand;
    }
    
    public String getBrand() {
        return brand;
    }
}

public final class Car extends Vehicle {
    private int numberOfDoors;
    
    public Car(String brand, int numberOfDoors) {
        super(brand);
        this.numberOfDoors = numberOfDoors;
    }
}

public final class Motorcycle extends Vehicle {
    private boolean hasSidecar;
    
    public Motorcycle(String brand, boolean hasSidecar) {
        super(brand);
        this.hasSidecar = hasSidecar;
    }
}

public final class Truck extends Vehicle {
    private double payloadCapacity;
    
    public Truck(String brand, double payloadCapacity) {
        super(brand);
        this.payloadCapacity = payloadCapacity;
    }
}

permits句の省略

許可されたサブクラスが同じソースファイル内に定義されている場合、permits句を省略できます。コンパイラが自動的に推論します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Shape.java - permits句を省略可能
public sealed class Shape {
    // Shapeクラスの本体
}

final class Circle extends Shape {
    double radius;
}

final class Rectangle extends Shape {
    double width, height;
}

final class Triangle extends Shape {
    double base, height;
}

ネストしたクラスでのpermits省略

ネストしたクラスとして定義する場合も、permits句を省略できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public sealed class Expression {
    
    public static final class Literal extends Expression {
        private final int value;
        
        public Literal(int value) {
            this.value = value;
        }
        
        public int getValue() {
            return value;
        }
    }
    
    public static final class Addition extends Expression {
        private final Expression left;
        private final Expression right;
        
        public Addition(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }
        
        public Expression getLeft() {
            return left;
        }
        
        public Expression getRight() {
            return right;
        }
    }
    
    public static final class Multiplication extends Expression {
        private final Expression left;
        private final Expression right;
        
        public Multiplication(Expression left, Expression right) {
            this.left = left;
            this.right = right;
        }
    }
}

異なるパッケージに配置する場合

許可されたサブクラスを異なるパッケージに配置する場合は、完全修飾名で指定し、permits句を明示する必要があります。

1
2
3
4
5
6
7
8
9
// com/example/geometry/Shape.java
package com.example.geometry;

public sealed class Shape 
    permits com.example.geometry.circle.Circle,
            com.example.geometry.quad.Rectangle,
            com.example.geometry.quad.Square {
    // クラス本体
}

ただし、以下の制約があります。

制約 説明
同一モジュール 名前付きモジュールの場合、sealedクラスと許可されたサブクラスは同一モジュール内に存在する必要がある
同一パッケージ 無名モジュールの場合、同一パッケージ内に存在する必要がある

コンパイルエラーの例

許可されていないクラスが継承しようとすると、コンパイルエラーになります。

1
2
3
4
5
6
7
8
9
public sealed class Animal permits Dog, Cat {
    // Animal本体
}

public final class Dog extends Animal { }
public final class Cat extends Animal { }

// コンパイルエラー: BirdはAnimalの許可されたサブクラスではない
public class Bird extends Animal { }  // エラー

継承先の3つの選択肢

sealedクラスを継承するクラスは、以下の3つの修飾子のいずれかを必ず指定する必要があります。

修飾子 説明 継承可否
final これ以上の継承を禁止 不可
sealed さらに限定的な継承を許可 限定的に可
non-sealed 継承制限を解除し、誰でも継承可能に

継承階層の概念図

flowchart TB
    A["sealed class Shape"]
    
    A --> B["final class Circle"]
    A --> C["sealed class Rectangle"]
    A --> D["non-sealed class FreeformShape"]
    
    C --> E["final class Square"]
    C --> F["final class RoundedRectangle"]
    
    D --> G["class Star"]
    D --> H["class Blob"]
    D --> I["任意のクラス..."]
    
    style A fill:#e1f5fe
    style B fill:#c8e6c9
    style C fill:#e1f5fe
    style D fill:#fff3e0
    style E fill:#c8e6c9
    style F fill:#c8e6c9

finalサブクラス

final修飾子を使用すると、そのクラスからの継承を完全に禁止します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public sealed class PaymentMethod permits CreditCard, BankTransfer, DigitalWallet {
    public abstract void processPayment(double amount);
}

// finalクラス - これ以上継承できない
public final class CreditCard extends PaymentMethod {
    private String cardNumber;
    private String expiryDate;
    
    public CreditCard(String cardNumber, String expiryDate) {
        this.cardNumber = cardNumber;
        this.expiryDate = expiryDate;
    }
    
    @Override
    public void processPayment(double amount) {
        System.out.println("クレジットカードで " + amount + "円を決済");
    }
}

public final class BankTransfer extends PaymentMethod {
    private String accountNumber;
    
    public BankTransfer(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public void processPayment(double amount) {
        System.out.println("銀行振込で " + amount + "円を送金");
    }
}

public final class DigitalWallet extends PaymentMethod {
    private String walletId;
    
    public DigitalWallet(String walletId) {
        this.walletId = walletId;
    }
    
    @Override
    public void processPayment(double amount) {
        System.out.println("電子ウォレットで " + amount + "円を決済");
    }
}

sealedサブクラス

サブクラス自体もsealedにして、さらに継承階層を制御できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public sealed class Account permits SavingsAccount, CheckingAccount, InvestmentAccount {
    protected double balance;
    
    public Account(double balance) {
        this.balance = balance;
    }
    
    public double getBalance() {
        return balance;
    }
}

// sealedサブクラス - さらに限定的な継承を許可
public sealed class InvestmentAccount extends Account 
    permits StockAccount, BondAccount, MutualFundAccount {
    
    protected double riskLevel;
    
    public InvestmentAccount(double balance, double riskLevel) {
        super(balance);
        this.riskLevel = riskLevel;
    }
}

public final class StockAccount extends InvestmentAccount {
    public StockAccount(double balance) {
        super(balance, 0.8);  // 高リスク
    }
}

public final class BondAccount extends InvestmentAccount {
    public BondAccount(double balance) {
        super(balance, 0.3);  // 低リスク
    }
}

public final class MutualFundAccount extends InvestmentAccount {
    public MutualFundAccount(double balance) {
        super(balance, 0.5);  // 中リスク
    }
}

public final class SavingsAccount extends Account {
    public SavingsAccount(double balance) {
        super(balance);
    }
}

public final class CheckingAccount extends Account {
    public CheckingAccount(double balance) {
        super(balance);
    }
}

non-sealedサブクラス

non-sealed修飾子を使用すると、その時点で継承制限が解除され、誰でも継承できるようになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public sealed class Plugin permits CorePlugin, CommunityPlugin {
    protected String name;
    
    public Plugin(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}

// finalクラス - 拡張不可
public final class CorePlugin extends Plugin {
    public CorePlugin(String name) {
        super(name);
    }
    
    public void executeCoreFunction() {
        System.out.println("コアプラグイン機能を実行: " + name);
    }
}

// non-sealedクラス - 誰でも拡張可能
public non-sealed class CommunityPlugin extends Plugin {
    public CommunityPlugin(String name) {
        super(name);
    }
    
    public void executeCommunityFunction() {
        System.out.println("コミュニティプラグイン機能を実行: " + name);
    }
}

// CommunityPluginは誰でも継承できる
public class MyCustomPlugin extends CommunityPlugin {
    public MyCustomPlugin(String name) {
        super(name);
    }
    
    public void executeCustomFunction() {
        System.out.println("カスタム機能を実行: " + getName());
    }
}

使い分けの指針

状況 推奨修飾子 理由
完全にクローズドな階層を作りたい final これ以上の拡張を防ぐ
階層的な制御を維持したい sealed 段階的な制限を適用
拡張ポイントを提供したい non-sealed サードパーティによる拡張を許可

Sealed Interfacesの活用

クラスだけでなく、インターフェースもsealedにできます。Sealed Interfacesは、実装クラスを制限し、型システムをより厳密に定義するのに役立ちます。

基本的なSealed Interface

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public sealed interface Result<T> permits Success, Failure {
    boolean isSuccess();
}

public record Success<T>(T value) implements Result<T> {
    @Override
    public boolean isSuccess() {
        return true;
    }
}

public record Failure<T>(String errorMessage) implements Result<T> {
    @Override
    public boolean isSuccess() {
        return false;
    }
}

Sealed Interfaceと継承階層

インターフェースの場合、permits句に指定できるのは以下の3種類です。

種類 説明
finalクラス そのインターフェースを実装する最終クラス
sealedクラス/インターフェース さらに制限された階層を形成
non-sealedクラス/インターフェース 制限を解除して自由な実装を許可
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public sealed interface JsonValue 
    permits JsonObject, JsonArray, JsonPrimitive {
    String toJsonString();
}

public final class JsonObject implements JsonValue {
    private final Map<String, JsonValue> properties;
    
    public JsonObject(Map<String, JsonValue> properties) {
        this.properties = Map.copyOf(properties);
    }
    
    @Override
    public String toJsonString() {
        return properties.entrySet().stream()
            .map(e -> "\"" + e.getKey() + "\":" + e.getValue().toJsonString())
            .collect(Collectors.joining(",", "{", "}"));
    }
}

public final class JsonArray implements JsonValue {
    private final List<JsonValue> elements;
    
    public JsonArray(List<JsonValue> elements) {
        this.elements = List.copyOf(elements);
    }
    
    @Override
    public String toJsonString() {
        return elements.stream()
            .map(JsonValue::toJsonString)
            .collect(Collectors.joining(",", "[", "]"));
    }
}

// さらにsealedで制限
public sealed interface JsonPrimitive extends JsonValue 
    permits JsonString, JsonNumber, JsonBoolean, JsonNull {
}

public record JsonString(String value) implements JsonPrimitive {
    @Override
    public String toJsonString() {
        return "\"" + value + "\"";
    }
}

public record JsonNumber(Number value) implements JsonPrimitive {
    @Override
    public String toJsonString() {
        return value.toString();
    }
}

public record JsonBoolean(boolean value) implements JsonPrimitive {
    @Override
    public String toJsonString() {
        return String.valueOf(value);
    }
}

public record JsonNull() implements JsonPrimitive {
    @Override
    public String toJsonString() {
        return "null";
    }
}

Recordとの組み合わせ

RecordクラスはデフォルトでImplicitlyにfinalなため、Sealed Interfaceとの相性が非常に良いです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public sealed interface Command permits CreateCommand, UpdateCommand, DeleteCommand {
    String getEntityId();
    Instant getTimestamp();
}

public record CreateCommand(
    String entityId,
    Map<String, Object> data,
    Instant timestamp
) implements Command {
    public CreateCommand {
        Objects.requireNonNull(entityId);
        Objects.requireNonNull(data);
        timestamp = timestamp != null ? timestamp : Instant.now();
    }
    
    @Override
    public String getEntityId() {
        return entityId;
    }
    
    @Override
    public Instant getTimestamp() {
        return timestamp;
    }
}

public record UpdateCommand(
    String entityId,
    Map<String, Object> changes,
    Instant timestamp
) implements Command {
    @Override
    public String getEntityId() {
        return entityId;
    }
    
    @Override
    public Instant getTimestamp() {
        return timestamp;
    }
}

public record DeleteCommand(
    String entityId,
    Instant timestamp
) implements Command {
    @Override
    public String getEntityId() {
        return entityId;
    }
    
    @Override
    public Instant getTimestamp() {
        return timestamp;
    }
}

パターンマッチングとの組み合わせ

Sealed Classesの真価は、パターンマッチングと組み合わせたときに発揮されます。コンパイラが許可されたサブクラスを把握しているため、網羅性チェック(exhaustiveness checking) が可能になります。

switch式での網羅性チェック

Java 21以降のパターンマッチングfor switchでは、Sealed Classesのすべてのサブタイプをカバーしていることをコンパイラが検証します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public sealed interface Shape permits Circle, Rectangle, Triangle {
    double area();
}

public record Circle(double radius) implements Shape {
    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

public record Rectangle(double width, double height) implements Shape {
    @Override
    public double area() {
        return width * height;
    }
}

public record Triangle(double base, double height) implements Shape {
    @Override
    public double area() {
        return 0.5 * base * height;
    }
}

public class ShapeProcessor {
    
    // defaultケース不要 - コンパイラが網羅性を保証
    public String describe(Shape shape) {
        return switch (shape) {
            case Circle c -> "半径 " + c.radius() + " の円";
            case Rectangle r -> r.width() + " x " + r.height() + " の長方形";
            case Triangle t -> "底辺 " + t.base() + "、高さ " + t.height() + " の三角形";
        };
    }
    
    public double calculateArea(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();
        };
    }
}

実行環境: Java 21以降(パターンマッチングfor switch正式版)

従来のif-elseとの比較

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 従来のif-else方式(非推奨)
public String describeOldStyle(Shape shape) {
    if (shape instanceof Circle) {
        Circle c = (Circle) shape;
        return "半径 " + c.radius() + " の円";
    } else if (shape instanceof Rectangle) {
        Rectangle r = (Rectangle) shape;
        return r.width() + " x " + r.height() + " の長方形";
    } else if (shape instanceof Triangle) {
        Triangle t = (Triangle) shape;
        return "底辺 " + t.base() + "、高さ " + t.height() + " の三角形";
    } else {
        // 新しいサブタイプが追加されてもコンパイルエラーにならない
        throw new IllegalArgumentException("未知の図形");
    }
}

// パターンマッチング方式(推奨)
public String describeModern(Shape shape) {
    return switch (shape) {
        case Circle c -> "半径 " + c.radius() + " の円";
        case Rectangle r -> r.width() + " x " + r.height() + " の長方形";
        case Triangle t -> "底辺 " + t.base() + "、高さ " + t.height() + " の三角形";
        // 新しいサブタイプが追加されるとコンパイルエラー
    };
}

Guardedパターンの活用

条件付きパターン(Guarded Pattern)を使用して、より細かい分岐も可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public String categorize(Shape shape) {
    return switch (shape) {
        case Circle c when c.radius() > 100 -> "大きな円";
        case Circle c when c.radius() > 10 -> "中くらいの円";
        case Circle c -> "小さな円";
        case Rectangle r when r.width() == r.height() -> "正方形";
        case Rectangle r -> "長方形";
        case Triangle t when t.base() == t.height() -> "二等辺三角形";
        case Triangle t -> "三角形";
    };
}

コンパイル時の安全性

Sealed Classesとパターンマッチングの組み合わせにより、以下の安全性が保証されます。

保証 説明
網羅性チェック すべてのサブタイプを処理しているか検証
新規サブタイプ追加時の警告 permits句に新しいクラスを追加すると、未処理のswitchでコンパイルエラー
不要なdefault句の排除 全サブタイプをカバーしていればdefaultは不要

代数的データ型の表現

Sealed ClassesとRecordを組み合わせることで、関数型プログラミングで一般的な代数的データ型(Algebraic Data Types: ADT) をJavaで表現できます。

代数的データ型とは

代数的データ型は、直積型(Product Types)直和型(Sum Types) を組み合わせたデータ型です。

Javaでの表現 説明
直積型 Record 複数の値を組み合わせた型(AND)
直和型 Sealed Interface/Class 複数の型のいずれか一つ(OR)
flowchart TB
    subgraph "代数的データ型"
        direction TB
        A["直和型(Sum Type)"]
        B["直積型(Product Type)"]
        
        A --> C["Option<T> = Some<T> | None"]
        A --> D["Result<T,E> = Ok<T> | Err<E>"]
        
        B --> E["record Point(int x, int y)"]
        B --> F["record Person(String name, int age)"]
    end

Option型の実装

値が存在するかしないかを表現するOption型を実装してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public sealed interface Option<T> permits Option.Some, Option.None {
    
    boolean isPresent();
    T get();
    T orElse(T defaultValue);
    <U> Option<U> map(Function<? super T, ? extends U> mapper);
    <U> Option<U> flatMap(Function<? super T, Option<U>> mapper);
    
    record Some<T>(T value) implements Option<T> {
        public Some {
            Objects.requireNonNull(value, "Some cannot contain null");
        }
        
        @Override
        public boolean isPresent() {
            return true;
        }
        
        @Override
        public T get() {
            return value;
        }
        
        @Override
        public T orElse(T defaultValue) {
            return value;
        }
        
        @Override
        public <U> Option<U> map(Function<? super T, ? extends U> mapper) {
            return new Some<>(mapper.apply(value));
        }
        
        @Override
        public <U> Option<U> flatMap(Function<? super T, Option<U>> mapper) {
            return mapper.apply(value);
        }
    }
    
    record None<T>() implements Option<T> {
        @Override
        public boolean isPresent() {
            return false;
        }
        
        @Override
        public T get() {
            throw new NoSuchElementException("None has no value");
        }
        
        @Override
        public T orElse(T defaultValue) {
            return defaultValue;
        }
        
        @Override
        @SuppressWarnings("unchecked")
        public <U> Option<U> map(Function<? super T, ? extends U> mapper) {
            return (Option<U>) this;
        }
        
        @Override
        @SuppressWarnings("unchecked")
        public <U> Option<U> flatMap(Function<? super T, Option<U>> mapper) {
            return (Option<U>) this;
        }
    }
    
    // ファクトリメソッド
    static <T> Option<T> of(T value) {
        return value != null ? new Some<>(value) : new None<>();
    }
    
    static <T> Option<T> empty() {
        return new None<>();
    }
}

使用例は以下の通りです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public class OptionDemo {
    public static void main(String[] args) {
        Option<String> some = Option.of("Hello");
        Option<String> none = Option.empty();
        
        // パターンマッチングで処理
        String result1 = switch (some) {
            case Option.Some<String> s -> "値あり: " + s.value();
            case Option.None<String> n -> "値なし";
        };
        System.out.println(result1);  // 値あり: Hello
        
        // メソッドチェーン
        String result2 = Option.of("World")
            .map(String::toUpperCase)
            .orElse("default");
        System.out.println(result2);  // WORLD
    }
}

Result型の実装

成功と失敗を表現するResult型も、代数的データ型の典型的な例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public sealed interface Result<T, E> permits Result.Ok, Result.Err {
    
    boolean isOk();
    boolean isErr();
    T getOrThrow() throws Exception;
    T orElse(T defaultValue);
    <U> Result<U, E> map(Function<? super T, ? extends U> mapper);
    <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> mapper);
    
    record Ok<T, E>(T value) implements Result<T, E> {
        @Override
        public boolean isOk() {
            return true;
        }
        
        @Override
        public boolean isErr() {
            return false;
        }
        
        @Override
        public T getOrThrow() {
            return value;
        }
        
        @Override
        public T orElse(T defaultValue) {
            return value;
        }
        
        @Override
        public <U> Result<U, E> map(Function<? super T, ? extends U> mapper) {
            return new Ok<>(mapper.apply(value));
        }
        
        @Override
        public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> mapper) {
            return mapper.apply(value);
        }
    }
    
    record Err<T, E>(E error) implements Result<T, E> {
        @Override
        public boolean isOk() {
            return false;
        }
        
        @Override
        public boolean isErr() {
            return true;
        }
        
        @Override
        public T getOrThrow() throws Exception {
            if (error instanceof Exception e) {
                throw e;
            }
            throw new RuntimeException(error.toString());
        }
        
        @Override
        public T orElse(T defaultValue) {
            return defaultValue;
        }
        
        @Override
        @SuppressWarnings("unchecked")
        public <U> Result<U, E> map(Function<? super T, ? extends U> mapper) {
            return (Result<U, E>) this;
        }
        
        @Override
        @SuppressWarnings("unchecked")
        public <U> Result<U, E> flatMap(Function<? super T, Result<U, E>> mapper) {
            return (Result<U, E>) this;
        }
    }
    
    // ファクトリメソッド
    static <T, E> Result<T, E> ok(T value) {
        return new Ok<>(value);
    }
    
    static <T, E> Result<T, E> err(E error) {
        return new Err<>(error);
    }
    
    // 例外をキャッチしてResultに変換
    static <T> Result<T, Exception> of(Supplier<T> supplier) {
        try {
            return new Ok<>(supplier.get());
        } catch (Exception e) {
            return new Err<>(e);
        }
    }
}

抽象構文木(AST)の表現

コンパイラやインタプリタで使用される抽象構文木も、代数的データ型で自然に表現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public sealed interface Expr permits Expr.Literal, Expr.Binary, Expr.Unary, Expr.Variable {
    
    record Literal(Object value) implements Expr {}
    
    record Binary(Expr left, String operator, Expr right) implements Expr {}
    
    record Unary(String operator, Expr operand) implements Expr {}
    
    record Variable(String name) implements Expr {}
}

public class Evaluator {
    private final Map<String, Object> variables;
    
    public Evaluator(Map<String, Object> variables) {
        this.variables = variables;
    }
    
    public Object evaluate(Expr expr) {
        return switch (expr) {
            case Expr.Literal lit -> lit.value();
            
            case Expr.Variable var -> {
                Object value = variables.get(var.name());
                if (value == null) {
                    throw new RuntimeException("未定義の変数: " + var.name());
                }
                yield value;
            }
            
            case Expr.Unary unary -> {
                Object operand = evaluate(unary.operand());
                yield switch (unary.operator()) {
                    case "-" -> -(Double) operand;
                    case "!" -> !(Boolean) operand;
                    default -> throw new RuntimeException("未知の単項演算子: " + unary.operator());
                };
            }
            
            case Expr.Binary bin -> {
                Object left = evaluate(bin.left());
                Object right = evaluate(bin.right());
                yield switch (bin.operator()) {
                    case "+" -> (Double) left + (Double) right;
                    case "-" -> (Double) left - (Double) right;
                    case "*" -> (Double) left * (Double) right;
                    case "/" -> (Double) left / (Double) right;
                    case "==" -> left.equals(right);
                    case "!=" -> !left.equals(right);
                    default -> throw new RuntimeException("未知の二項演算子: " + bin.operator());
                };
            }
        };
    }
}

ユースケースと設計パターン

状態マシンの表現

状態遷移を型で表現することで、不正な状態遷移をコンパイル時に防止できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public sealed interface OrderState 
    permits OrderState.Created, OrderState.Paid, OrderState.Shipped, 
            OrderState.Delivered, OrderState.Cancelled {
    
    record Created(Instant createdAt) implements OrderState {}
    
    record Paid(Instant createdAt, Instant paidAt, String paymentId) 
        implements OrderState {}
    
    record Shipped(Instant createdAt, Instant paidAt, String paymentId, 
                   Instant shippedAt, String trackingNumber) 
        implements OrderState {}
    
    record Delivered(Instant createdAt, Instant paidAt, String paymentId,
                     Instant shippedAt, String trackingNumber, 
                     Instant deliveredAt) 
        implements OrderState {}
    
    record Cancelled(Instant createdAt, Instant cancelledAt, String reason) 
        implements OrderState {}
}

public class Order {
    private final String orderId;
    private OrderState state;
    
    public Order(String orderId) {
        this.orderId = orderId;
        this.state = new OrderState.Created(Instant.now());
    }
    
    public Order pay(String paymentId) {
        this.state = switch (state) {
            case OrderState.Created c -> 
                new OrderState.Paid(c.createdAt(), Instant.now(), paymentId);
            default -> 
                throw new IllegalStateException("支払いは作成済み状態からのみ可能");
        };
        return this;
    }
    
    public Order ship(String trackingNumber) {
        this.state = switch (state) {
            case OrderState.Paid p -> 
                new OrderState.Shipped(p.createdAt(), p.paidAt(), p.paymentId(),
                                       Instant.now(), trackingNumber);
            default -> 
                throw new IllegalStateException("発送は支払い済み状態からのみ可能");
        };
        return this;
    }
    
    public Order deliver() {
        this.state = switch (state) {
            case OrderState.Shipped s -> 
                new OrderState.Delivered(s.createdAt(), s.paidAt(), s.paymentId(),
                                        s.shippedAt(), s.trackingNumber(), 
                                        Instant.now());
            default -> 
                throw new IllegalStateException("配達は発送済み状態からのみ可能");
        };
        return this;
    }
    
    public Order cancel(String reason) {
        this.state = switch (state) {
            case OrderState.Created c -> 
                new OrderState.Cancelled(c.createdAt(), Instant.now(), reason);
            case OrderState.Paid p -> 
                new OrderState.Cancelled(p.createdAt(), Instant.now(), reason);
            default -> 
                throw new IllegalStateException("配送後のキャンセルは不可");
        };
        return this;
    }
    
    public String getStatus() {
        return switch (state) {
            case OrderState.Created c -> "作成済み";
            case OrderState.Paid p -> "支払い済み";
            case OrderState.Shipped s -> "発送済み(追跡: " + s.trackingNumber() + ")";
            case OrderState.Delivered d -> "配達完了";
            case OrderState.Cancelled c -> "キャンセル済み(理由: " + c.reason() + ")";
        };
    }
}

Visitorパターンの代替

従来のVisitorパターンは、Sealed Classesとパターンマッチングで簡潔に置き換えられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 従来のVisitorパターン
public interface ShapeVisitor<R> {
    R visitCircle(Circle circle);
    R visitRectangle(Rectangle rectangle);
    R visitTriangle(Triangle triangle);
}

public interface Shape {
    <R> R accept(ShapeVisitor<R> visitor);
}

// Sealed Classes + パターンマッチングによる代替
public sealed interface Shape permits Circle, Rectangle, Triangle {
    // accept()メソッドは不要
}

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 {}

// 処理はswitch式で記述
public class ShapeOperations {
    
    public double calculateArea(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();
        };
    }
    
    public double calculatePerimeter(Shape shape) {
        return switch (shape) {
            case Circle c -> 2 * Math.PI * c.radius();
            case Rectangle r -> 2 * (r.width() + r.height());
            case Triangle t -> {
                // 簡略化のため、正三角形と仮定
                double side = t.base();
                yield 3 * side;
            }
        };
    }
    
    public String toSvg(Shape shape) {
        return switch (shape) {
            case Circle c -> 
                "<circle r=\"" + c.radius() + "\" />";
            case Rectangle r -> 
                "<rect width=\"" + r.width() + "\" height=\"" + r.height() + "\" />";
            case Triangle t -> 
                "<polygon points=\"0," + t.height() + " " + 
                (t.base() / 2) + ",0 " + t.base() + "," + t.height() + "\" />";
        };
    }
}

APIレスポンスの型安全な表現

Web APIのレスポンスを型安全に表現する例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public sealed interface ApiResponse<T> 
    permits ApiResponse.Success, ApiResponse.ClientError, ApiResponse.ServerError {
    
    int statusCode();
    
    record Success<T>(int statusCode, T data, Map<String, String> headers) 
        implements ApiResponse<T> {
        
        public Success(T data) {
            this(200, data, Map.of());
        }
    }
    
    record ClientError<T>(int statusCode, String message, List<String> errors) 
        implements ApiResponse<T> {
        
        public static <T> ClientError<T> badRequest(String message) {
            return new ClientError<>(400, message, List.of());
        }
        
        public static <T> ClientError<T> notFound(String message) {
            return new ClientError<>(404, message, List.of());
        }
        
        public static <T> ClientError<T> validationError(List<String> errors) {
            return new ClientError<>(422, "Validation failed", errors);
        }
    }
    
    record ServerError<T>(int statusCode, String message, String traceId) 
        implements ApiResponse<T> {
        
        public static <T> ServerError<T> internalError(String traceId) {
            return new ServerError<>(500, "Internal Server Error", traceId);
        }
    }
}

public class ResponseHandler {
    
    public <T> void handle(ApiResponse<T> response) {
        switch (response) {
            case ApiResponse.Success<T> s -> {
                System.out.println("成功: " + s.data());
                s.headers().forEach((k, v) -> 
                    System.out.println("  " + k + ": " + v));
            }
            
            case ApiResponse.ClientError<T> c -> {
                System.err.println("クライアントエラー (" + c.statusCode() + "): " + c.message());
                if (!c.errors().isEmpty()) {
                    c.errors().forEach(e -> System.err.println("  - " + e));
                }
            }
            
            case ApiResponse.ServerError<T> s -> {
                System.err.println("サーバーエラー (" + s.statusCode() + "): " + s.message());
                System.err.println("トレースID: " + s.traceId());
            }
        }
    }
}

まとめ

Sealed Classesは、Javaにおける継承制御の新しいパラダイムです。本記事で解説した内容を振り返ります。

項目 ポイント
基本概念 sealed修飾子とpermits句で継承先を明示的に制限
3つの選択肢 サブクラスはfinalsealednon-sealedのいずれかを選択
Sealed Interfaces インターフェースもsealedにでき、Recordとの相性が良い
パターンマッチング switch式での網羅性チェックにより、型安全なコードを実現
代数的データ型 関数型プログラミングの概念をJavaで自然に表現可能
設計パターン 状態マシン、Visitorパターンの代替、型安全なAPIレスポンスなど多様なユースケース

Sealed Classesを活用することで、以下のメリットが得られます。

  • 継承階層の意図を明確にコードで表現できる
  • パターンマッチングと組み合わせて網羅性を保証できる
  • 新しいサブタイプ追加時にコンパイルエラーで漏れを検出できる
  • 代数的データ型によりドメインモデルを型安全に表現できる

Java 17以降を使用している場合は、積極的にSealed Classesを活用して、より安全で保守性の高いコードを書いていきましょう。

参考リンク