この記事について
Java15からpreview機能としてテストされ,Java17で正式に導入された sealed class について紹介します.クラスやインターフェースを継承・実装できるものを制限できます.
以下のように,既存の要素を変更するものではありません.
-
friend
のような新しいアクセス制御を提供するものではない -
final
のはたらきを変更するものでは決してない
出展
この記事は基本的に JEP 409 に基づきます.また,各所でJava言語仕様を参照しています.
Sealedクラスの宣言
クラスに sealed
修飾子をつけるとSealedクラスになります.さらに,extends
節やimplements
節の後にpermits
節を付けて,自身を継承可能なクラスを明示できます.
次の例では,Shape
を継承できるのは Circle
Rectangle
Square
の3つです.
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square { ... }
permits
節の制約
permits
に指定するクラス名は正準名(canonical name)のみ使用可能です.従って匿名クラス1やローカルクラスは指定できません.
permits
指定されたクラス(以下では「許可されたサブクラス」と呼びます)はスーパークラスの「近く」にある必要があります.具体的には,
- 同じモジュール内に存在すること
- 名前なしモジュールの場合は,さらに同じパッケージに所属すること
が要求されます.
例えば,同じ名前付きモジュール内であれば,次のように別パッケージのクラスも指定できます.
package com.example.geometry;
public abstract sealed class Shape
permits com.example.polar.Circle,
com.example.quad.Rectangle,
com.example.quad.simple.Square { ... }
暗黙的な permits
permits
節を省略することで,同じファイル内にあるクラス(内部クラスも含む)を自動的に2許可対象とすることができます.
例えば,次のような書き方も可能です.
abstract sealed class Root { ...
final class A extends Root { ... }
final class B extends Root { ... }
final class C extends Root { ... }
}
暗黙的に許可されるサブクラスが1つも存在しない場合,コンパイルエラーになります.3
サブクラスにかかる制約
許可されたサブクラスは,元の sealed
クラスを直接継承する必要があります.また,下記のようなスーパークラスの sealing を伝搬するかどうかを示す修飾子を必ずつけなければいけません.具体的には,
- それ以上の継承を許さない場合は従来通りの
final
- さらにサブクラスを制限する場合には,スーパークラスと同様の
sealed
- さらなるサブクラスに制限を付けない場合には,
non-sealed
4 5
の修飾子のどれか1つだけを付与します.
例えば,先ほどの例を拡張して
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square, WeirdShape { ... }
public final class Circle extends Shape { ... }
public sealed class Rectangle extends Shape
permits TransparentRectangle, FilledRectangle { ... }
public final class TransparentRectangle extends Rectangle { ... }
public final class FilledRectangle extends Rectangle { ... }
public final class Square extends Shape { ... }
public non-sealed class WeirdShape extends Shape { ... }
この中で WeirdShape だけは,別モジュールのクラスが継承できます.
sealed
クラスと non-sealed
クラスは抽象クラスにすることができます.従来通り final abstract
なクラスは作れません.
アクセス権の制約
permits
節でクラス名を使用するので,許可されたサブクラスとSealedスーパークラスは互いにアクセス可能である必要があります.しかし同じアクセスレベルを持つ必要はありません.
例えばswitch
パターンマッチングが導入されれば,アクセスレベルが低いサブクラスがある場合,別モジュール等からの使用時は default
を省略できないことになります.
Sealedインターフェース
クラスと同様にインターフェースにも sealed
修飾子を付与できます.実装クラス及びサブインターフェースも,同様に permits
節で指定できます.
例えば,次のようになります.
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }
public final class ConstantExpr implements Expr { ... }
public final class PlusExpr implements Expr { ... }
public final class TimesExpr implements Expr { ... }
public final class NegExpr implements Expr { ... }
アノテーションインターフェースに sealed
は指定できません.( JLS 9.6 )
sealed
インターフェースは関数型インターフェースとはみなされません.( JLS 9.8 )
レコード型との関連
Java16 で導入された record
クラスは暗黙に final
なので,sealed
インターフェースを実装できます.
例えば,次のように簡単に書けます.
package com.example.expression;
public sealed interface Expr
permits ConstantExpr, PlusExpr, TimesExpr, NegExpr { ... }
public record ConstantExpr(int i) implements Expr { ... }
public record PlusExpr(Expr a, Expr b) implements Expr { ... }
public record TimesExpr(Expr a, Expr b) implements Expr { ... }
public record NegExpr(Expr e) implements Expr { ... }
この組み合わせによっていわゆる「代数的データ型」が実現できます.record
クラスが直積型,sealed
クラスが直和型の表現になっています.
列挙型との関連 6
以前から,enum
には abstract
, final
の修飾子はつけられなかったのですが,今回追加された sealed
, non-sealed
も列挙型に明示的に指定することはできません.
また,従来の暗黙的な final
としての扱いは次のように変更されました.
- クラスボディを持つ列挙定数がない場合は,暗黙的に
final
(従来通り) - クラスボディを持つ列挙定数が1つ以上ある場合は,暗黙的に
sealed
- 許可されるサブクラスは,対応して暗黙に宣言される匿名クラス
- クラスボディを持つ列挙定数に対応する匿名クラスは,暗黙に
final
(従来通り)
このように,列挙型は暗黙に final
あるいは sealed
なので,他のsealedインターフェースを実装できます. ( JLS 8.1.1.2 )
型変換との関連
以前から,final
クラスの制約により絶対に成り立たない instanceof
式はコンパイルエラーになっていました.例えば,
interface I {}
final class C {}
void test (C c) {
if (c instanceof I) // コンパイルエラー!
System.out.println("It's an I");
}
というコードは,「C
またはそのサブクラスがI
を実装する」ことは起こりえないので,コンパイルエラーになります7.
これと同じように,sealed
クラス及びそのすべてのサブクラスが(子孫に non-sealed
がなくて)インターフェース I
を実装しないことが明らかであるとき8は,instanceof I
はコンパイルエラーになります.例えば,
interface I {}
sealed class C permits D {}
final class D extends C {}
void test (C c) {
if (c instanceof I) // コンパイルエラー!
System.out.println("It's an I");
}
というコードは,C
及びそのサブクラスがI
を実装することはあり得ないのでコンパイルエラーになります.この例でもし D
が non-sealed
であれば,エラーにはなりません.
パターンマッチングとの関連
Sealedクラスは,Java17時点ではpreview機能として提供されているswitch
パターンマッチングと組み合わせることでとても有用です.現状で利用可能なif-else文では,例えば
class Shaper permits Circle, Rectangle, Square { ... }
Shape rotate(Shape shape, double angle) {
if (shape instanceof Circle) return shape;
else if (shape instanceof Rectangle) return shape.rotate(angle);
else if (shape instanceof Square) return shape.rotate(angle);
else throw new IncompatibleClassChangeError();
}
のようになり,すべての可能性を網羅しているにも関わらず最後のelse節を省略することはできません.しかし,switch
パターンマッチングの導入後は
// preview機能: Java17 では --enable-preview が必要
Shape rotate(Shape shape, double angle) {
return switch (shape) { // pattern matching switch
case Circle c -> c;
case Rectangle r -> shape.rotate(angle);
case Square s -> shape.rotate(angle);
// no default needed!
}
}
のように書ける見込みです.
その他の詳細情報
キーワードの追加
sealed
, non-sealed
, permits
が文脈依存キーワード(制限付き識別子)に追加されました.sealed
やpermits
という名前のクラス等の宣言はコンパイルエラーになります.
文法の変更
- クラス修飾子
sealed
non-sealed
の追加 - クラス宣言に
permits
節を追加
JVMの変更
-
sealed
クラスは修飾子ではなく新属性PermittedSubclasses
で表現され,すべての許可されたサブクラスを明示的に列挙します - 許可されたサブクラスの class ファイルには変更はありません
APIの変更
java.lang.Class
に次のメソッドが追加されました.
Class<?>[] getPermittedSubclasses()
boolean isSealed()
以上です.
-
匿名クラス宣言時に,スーパークラスとして
sealed
クラスを指定するとコンパイルエラーになります.( JLS 15.9.1 ) ↩ -
厳密には,
sealed
クラスC
がpermits
節を持たない場合,同じCompilation Unitで宣言されているクラスのうちC
を直接継承しているものだけが,C
の直接のサブクラスとして認められます.( JLS 8.1.6 ) ↩ -
JLS 8.1.6 に記載.
permits
節には1つ以上のクラスを指定しなければならないことと合わせて,許可対象が0個(つまりfinal
と同等)のSealedクラスを作ることはできません. ↩ -
non-sealed
はSealedクラス・Sealedインターフェースの許可されたサブクラス等にのみ付与可能です.他のクラス・インターフェースにnon-sealed
を付けると,コンパイルエラーになります.( JLS 8.1.1.2 ) ↩ -
JDK Issue 8223002 で,ハイフン入りキーワード(hyphenated keyword)の導入について議論されています.
non-sealed
はこの最初の実現例になりました. ↩ -
逆に,必ず成り立つ
instanceof
は,後方互換性の都合でパターンマッチングの場合にのみ禁止されています.これについては別の記事でも紹介しています. ↩ -
これを実現するために参照のナローイング変換の定義が変更され,2つのクラス・インターフェースが「互いに素(disjoint)である」という概念が導入されています.( JLS 5.1.6.1 ) ↩