3 シール・クラス

シール・クラスおよびインタフェースは、それらを拡張または実装できる他のクラスまたはインタフェースを制限します。

シール・クラスおよびインタフェースに関する背景情報は、JEP 409を参照してください。

継承の主な目的の1つは、コードの再利用です。新しいクラスを作成し、必要なコードの一部を含むクラスがすでに存在する場合は、既存のクラスから新しいクラスを導出できます。これにより、既存のクラスのフィールドおよびメソッドを自分で記述(およびデバッグ)しなくても再利用できます。

ただし、エンティティを定義し、これらのエンティティが相互にどのように関係するかを決定することで、ドメインに存在する様々な可能性をモデリングする場合はどうすればよいでしょうか。たとえば、グラフィック・ライブラリで作業しているとします。ライブラリが円や正方形などの一般的なジオメトリ・プリミティブをどのように扱うかを決定する必要があります。これらのジオメトリ・プリミティブを拡張できるShapeクラスを作成しました。ただし、任意のクラスによるShapeの拡張は許可しません。ライブラリのクライアントがそれ以上のプリミティブを宣言することは望ましくありません。クラスをシールすることで、クラスの拡張を許可するクラスを指定し、他の任意のクラスによる拡張を防止できます。

シール・クラスの宣言

クラスをシールするには、その宣言にsealed修飾子を追加します。次に、extends句およびimplements句の後に、permits句を追加します。この句によって、シール・クラスを拡張できるクラスが指定されます。

たとえば、Shapeの次の宣言では、許可される3つのサブクラスCircleSquareおよびRectangleが指定されています:

図3-1 Shape.java

public sealed class Shape
    permits Circle, Square, Rectangle {
}

許可されている次の3つのサブクラス(CircleSquareおよびRectangle)を、シール・クラスと同じモジュールまたは同じパッケージに定義します:

図3-2 Circle.java

public final class Circle extends Shape {
    public float radius;
}

図3-3 Square.java

Square非シール・クラスです。このタイプのクラスは、「許可されるサブクラスの制約」で説明されています。

public non-sealed class Square extends Shape {
   public double side;
}   

図3-4 Rectangle.java

public sealed class Rectangle extends Shape permits FilledRectangle {
    public double length, width;
}

Rectangleには、さらにサブクラスFilledRectangleがあります:

図3-5 FilledRectangle.java

public final class FilledRectangle extends Rectangle {
    public int red, green, blue;
}

または、シール・クラスと同じファイルに許可されたサブクラスを定義することもできます。これを行う場合は、permits句を省略できます:

package com.example.geometry;

public sealed class Figure
    // The permits clause has been omitted
    // as its permitted classes have been
    // defined in the same file.
{ }

final class Circle extends Figure {
    float radius;
}
non-sealed class Square extends Figure {
    float side;
}
sealed class Rectangle extends Figure {
    float length, width;
}
final class FilledRectangle extends Rectangle {
    int red, green, blue;
}

許可されるサブクラスの制約

許可されるサブクラスには、次の制約があります:

  • これらは、コンパイル時にシール・クラスからアクセスできる必要があります。

    たとえば、Shape.javaをコンパイルするには、コンパイラがShapeのすべての許可されたクラス、Circle.javaSquare.javaおよびRectangle.javaにアクセスできる必要があります。また、Rectangleはシール・クラスであるため、コンパイラはFilledRectangle.javaにアクセスする必要もあります。

  • シール・クラスを直接拡張する必要があります。

  • スーパークラスによって開始されたシールを続行する方法を記述するには、次の修飾子のいずれかのみが必要です:

    • final: これ以上拡張できません

    • sealed: 許可されたサブクラスによってのみ拡張できます

    • non-sealed: 不明なサブクラスによって拡張できます。シール・クラスは、許可されたサブクラスがこれを実行することを防ぐことはできません

    たとえば、Shapeの許可されたサブクラスは、Circlefinalで、Rectangleがsealedで、Squarenon-sealedである3つの修飾子をそれぞれ示しています。

  • これらは、シール・クラスと同じモジュール(シール・クラスが名前付きモジュール内にある場合)または同じパッケージ(Shape.javaの例のように、シール・クラスが名前のないモジュール内にある場合)にある必要があります。

    たとえば、次のcom.example.graphics.Shapeの宣言では、許可されたサブクラスはすべて異なるパッケージにあります。この例は、Shapeとその許可されたすべてのサブクラスが同じ名前付きモジュール内にある場合にのみコンパイルされます。

    package com.example.graphics;
    
    public sealed class Shape 
        permits com.example.polar.Circle,
                com.example.quad.Rectangle,
                com.example.quad.simple.Square { }

シールされたインタフェースの宣言

シール・クラスと同様に、インタフェースをシールするには、その宣言にsealed修飾子を追加します。次に、extends句の後に、シールされたインタフェースを実装できるクラスと、シールされたインタフェースを拡張できるインタフェースを指定するpermits句を追加します。

次の例では、Exprというシールされたインタフェースを宣言しています。クラスConstantExprPlusExprTimesExprおよびNegExprのみが実装できます:

package com.example.expressions;

public class TestExpressions {
  public static void main(String[] args) {
    // (6 + 7) * -8
    System.out.println(
      new TimesExpr(
        new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
        new NegExpr(new ConstantExpr(8))
      ).eval());
   }
}

sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

final class ConstantExpr implements Expr {
    int i;
    ConstantExpr(int i) { this.i = i; }
    public int eval() { return i; }
}

final class PlusExpr implements Expr {
    Expr a, b;
    PlusExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() + b.eval(); }
}

final class TimesExpr implements Expr {
    Expr a, b;
    TimesExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() * b.eval(); }
}

final class NegExpr implements Expr {
    Expr e;
    NegExpr(Expr e) { this.e = e; }
    public int eval() { return -e.eval(); }
}

許可されたサブクラスとしてのレコード・クラス

シール・クラスまたはインタフェースのpermits句でレコード・クラスに名前を付けることができます。詳細は、レコード・クラスを参照してください。

レコード・クラスは暗黙的にfinalであるため、通常のクラスではなくレコード・クラスを使用して前述の例を実装できます:

package com.example.records.expressions;

public class TestExpressions {
  public static void main(String[] args) {
    // (6 + 7) * -8
    System.out.println(
      new TimesExpr(
        new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
        new NegExpr(new ConstantExpr(8))
      ).eval());
   }
}

sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

record ConstantExpr(int i) implements Expr {
    public int eval() { return i(); }
}

record PlusExpr(Expr a, Expr b) implements Expr {
    public int eval() { return a.eval() + b.eval(); }
}

record TimesExpr(Expr a, Expr b) implements Expr {
    public int eval() { return a.eval() * b.eval(); }
}

record NegExpr(Expr e) implements Expr {
    public int eval() { return -e.eval(); }
}

参照型の縮小変換および非結合型

参照型の縮小変換は、型チェックのキャスト式で使用される変換の1つです。これにより、参照型Sの式を別の参照型Tの式として扱うことができます。STのサブタイプではありません。参照型の縮小変換では、S型の値がT型の正当な値であることを検証するために、実行時にテストが必要になる場合があります。ただし、どちらの型の値も存在しないことを静的に証明できる場合、特定の型のペア間の変換が禁止される制限があります。

次に例を示します。

public interface Polygon { }
public class Rectangle implements Polygon { }

public void work(Rectangle r) {
    Polygon p = (Polygon) r;      
}

キャスト式Polygon p = (Polygon) rは、RectanglerPolygon型である可能性があるため許可されます。RectanglePolygonのサブタイプです。ただし、次の例を考えてみます:

public interface Polygon { }
public class Triangle { }

public void work(Triangle t) {
    Polygon p = (Polygon) t;
}

クラスTriangleとインタフェースPolygonは無関係ですが、実行時にこれらの型が関連付けられる可能性があるため、この場合もキャスト式Polygon p = (Polygon) tが許可されます。開発者が次のクラスを宣言できます:

class MeshElement extends Triangle implements Polygon { }

ただし、コンパイラが、2つの型間で共有される値(null参照以外)がないと推定する場合もあります。このような型は非結合とみなされます。次に例を示します:

public interface Polygon { }
public final class UtahTeapot { }

public void work(UtahTeapot u) {
    Polygon p = (Polygon) u;  // Error: The cast can never succeed as
                              // UtahTeapot and Polygon are disjoint
}

クラスUtahTeapotfinalであるため、クラスをPolygonUtahTeapotの両方の子孫にすることはできません。したがって、PolygonUtahTeapotは非結合であり、キャスト文Polygon p = (Polygon) uは許可されません。

コンパイラが強化され、キャスト文が許可されるかどうかを確認するためにシール階層をナビゲートできるようになりました。次に例を示します:

public sealed interface Shape permits Polygon { }
public non-sealed interface Polygon extends Shape { }
public final class UtahTeapot { }
public class Ring { }

public void work(Shape s) {
    UtahTeapot u = (UtahTeapot) s;  // Error
    Ring r = (Ring) s;              // Permitted
}

最初のキャスト文UtahTeapot u = (UtahTeapot) sは許可されません。Shapesealedであるため、ShapeにはPolygonのみを指定できます。ただし、Polygonnon-sealedであるため、拡張できます。ただし、UtahTeapotfinalであるため、Polygonの潜在的なサブタイプはUtahTeapotを拡張できません。したがって、ShapeUtahTeapotにすることはできません。

対照的に、2番目のキャスト文Ring r = (Ring) sは許可されます。Ringfinalクラスではないため、ShapeRingにできます。

シール・クラスおよびインタフェースに関連するAPI

クラスjava.lang.Classには、シール・クラスおよびインタフェースに関連する次の2つの新しいメソッドがあります:

  • java.lang.constant.ClassDesc[] permittedSubclasses(): シールされている場合、クラスの許可されたすべてのサブクラスを表すjava.lang.constant.ClassDescオブジェクトを含む配列を返します。クラスがシールされていない場合は、空の配列を返します
  • boolean isSealed(): 指定されたクラスまたはインタフェースがシールされている場合はtrueを返し、それ以外の場合はfalseを返します