この記事について
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-sealed4 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という名前のクラス等の宣言はコンパイルエラーになります.
文法の変更
- クラス修飾子
sealednon-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 ) ↩