2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Java17] 新機能 Sealed Classes の詳細

Posted at

この記事について

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許可対象とすることができます.

例えば,次のような書き方も可能です.

Root.java
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を実装することはあり得ないのでコンパイルエラーになります.この例でもし Dnon-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 が文脈依存キーワード(制限付き識別子)に追加されました.sealedpermitsという名前のクラス等の宣言はコンパイルエラーになります.

文法の変更

  • クラス修飾子 sealed non-sealed の追加
  • クラス宣言に permits 節を追加

JVMの変更

  • sealed クラスは修飾子ではなく新属性 PermittedSubclasses で表現され,すべての許可されたサブクラスを明示的に列挙します
  • 許可されたサブクラスの class ファイルには変更はありません

APIの変更

java.lang.Class に次のメソッドが追加されました.

  • Class<?>[] getPermittedSubclasses()
  • boolean isSealed()

以上です.

  1. 匿名クラス宣言時に,スーパークラスとしてsealedクラスを指定するとコンパイルエラーになります.( JLS 15.9.1 )

  2. 厳密には,sealedクラスCpermits節を持たない場合,同じCompilation Unitで宣言されているクラスのうちCを直接継承しているものだけが,Cの直接のサブクラスとして認められます.( JLS 8.1.6 )

  3. JLS 8.1.6 に記載. permits 節には1つ以上のクラスを指定しなければならないことと合わせて,許可対象が0個(つまりfinalと同等)のSealedクラスを作ることはできません.

  4. non-sealed はSealedクラス・Sealedインターフェースの許可されたサブクラス等にのみ付与可能です.他のクラス・インターフェースに non-sealed を付けると,コンパイルエラーになります.( JLS 8.1.1.2 )

  5. JDK Issue 8223002 で,ハイフン入りキーワード(hyphenated keyword)の導入について議論されています.non-sealedはこの最初の実現例になりました.

  6. 列挙型の変更については JEPに記載が無いため,主に JLS 8.9 の情報です.

  7. 逆に,必ず成り立つinstanceofは,後方互換性の都合でパターンマッチングの場合にのみ禁止されています.これについては別の記事でも紹介しています.

  8. これを実現するために参照のナローイング変換の定義が変更され,2つのクラス・インターフェースが「互いに素(disjoint)である」という概念が導入されています.( JLS 5.1.6.1 )

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?