はじめに#
Javaにおける継承は、コードの再利用と拡張性を実現する強力な機能です。しかし、従来のJavaでは継承を「完全に禁止(final)」するか「完全に許可」するかの二択しかありませんでした。
Sealed Classes(シールドクラス) は、Java 17で正式導入された機能で、「どのクラスが継承できるか」を明示的に制御できます。これにより、クラス階層を厳密に管理し、より安全で予測可能な型設計が可能になります。
この記事では、Sealed Classesの基本概念から、permits句による継承先の制限、継承先の3つの選択肢(final、sealed、non-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:#c8e6c9finalサブクラス#
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)"]
endOption型の実装#
値が存在するかしないかを表現する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つの選択肢 |
サブクラスはfinal、sealed、non-sealedのいずれかを選択 |
| Sealed Interfaces |
インターフェースもsealedにでき、Recordとの相性が良い |
| パターンマッチング |
switch式での網羅性チェックにより、型安全なコードを実現 |
| 代数的データ型 |
関数型プログラミングの概念をJavaで自然に表現可能 |
| 設計パターン |
状態マシン、Visitorパターンの代替、型安全なAPIレスポンスなど多様なユースケース |
Sealed Classesを活用することで、以下のメリットが得られます。
- 継承階層の意図を明確にコードで表現できる
- パターンマッチングと組み合わせて網羅性を保証できる
- 新しいサブタイプ追加時にコンパイルエラーで漏れを検出できる
- 代数的データ型によりドメインモデルを型安全に表現できる
Java 17以降を使用している場合は、積極的にSealed Classesを活用して、より安全で保守性の高いコードを書いていきましょう。
参考リンク#