3 シール・クラス
シール・クラスおよびインタフェースは、それらを拡張または実装できる他のクラスまたはインタフェースを制限します。
シール・クラスおよびインタフェースに関する背景情報は、JEP 409を参照してください。
継承の主な目的の1つは、コードの再利用です。新しいクラスを作成し、必要なコードの一部を含むクラスがすでに存在する場合は、既存のクラスから新しいクラスを導出できます。これにより、既存のクラスのフィールドおよびメソッドを自分で記述(およびデバッグ)しなくても再利用できます。
ただし、エンティティを定義し、これらのエンティティが相互にどのように関係するかを決定することで、ドメインに存在する様々な可能性をモデリングする場合はどうすればよいでしょうか。たとえば、グラフィック・ライブラリで作業しているとします。ライブラリが円や正方形などの一般的なジオメトリ・プリミティブをどのように扱うかを決定する必要があります。これらのジオメトリ・プリミティブを拡張できるShape
クラスを作成しました。ただし、任意のクラスによるShape
の拡張は許可しません。ライブラリのクライアントがそれ以上のプリミティブを宣言することは望ましくありません。クラスをシールすることで、クラスの拡張を許可するクラスを指定し、他の任意のクラスによる拡張を防止できます。
シール・クラスの宣言
クラスをシールするには、その宣言にsealed
修飾子を追加します。次に、extends
句およびimplements
句の後に、permits
句を追加します。この句によって、シール・クラスを拡張できるクラスが指定されます。
たとえば、Shape
の次の宣言では、許可される3つのサブクラスCircle
、Square
およびRectangle
が指定されています:
図3-1 Shape.java
public sealed class Shape
permits Circle, Square, Rectangle {
}
許可されている次の3つのサブクラス(Circle
、Square
および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.java
、Square.java
およびRectangle.java
にアクセスできる必要があります。また、Rectangle
はシール・クラスであるため、コンパイラはFilledRectangle.java
にアクセスする必要もあります。 -
シール・クラスを直接拡張する必要があります。
-
スーパークラスによって開始されたシールを続行する方法を記述するには、次の修飾子のいずれかのみが必要です:
-
final
: これ以上拡張できません -
sealed
: 許可されたサブクラスによってのみ拡張できます -
non-sealed
: 不明なサブクラスによって拡張できます。シール・クラスは、許可されたサブクラスがこれを実行することを防ぐことはできません
たとえば、
Shape
の許可されたサブクラスは、Circle
がfinal
で、Rectangle
がsealedで、Square
がnon-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
というシールされたインタフェースを宣言しています。クラスConstantExpr
、PlusExpr
、TimesExpr
および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
の式として扱うことができます。S
はT
のサブタイプではありません。参照型の縮小変換では、S
型の値がT
型の正当な値であることを検証するために、実行時にテストが必要になる場合があります。ただし、どちらの型の値も存在しないことを静的に証明できる場合、特定の型のペア間の変換が禁止される制限があります。
次に例を示します。
public interface Polygon { }
public class Rectangle implements Polygon { }
public void work(Rectangle r) {
Polygon p = (Polygon) r;
}
キャスト式Polygon p = (Polygon) r
は、Rectangle
値r
がPolygon
型である可能性があるため許可されます。Rectangle
はPolygon
のサブタイプです。ただし、次の例を考えてみます:
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
}
クラスUtahTeapot
はfinal
であるため、クラスをPolygon
とUtahTeapot
の両方の子孫にすることはできません。したがって、Polygon
とUtahTeapot
は非結合であり、キャスト文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
は許可されません。Shape
がsealed
であるため、Shape
にはPolygon
のみを指定できます。ただし、Polygon
はnon-sealed
であるため、拡張できます。ただし、UtahTeapot
はfinal
であるため、Polygon
の潜在的なサブタイプはUtahTeapot
を拡張できません。したがって、Shape
をUtahTeapot
にすることはできません。
対照的に、2番目のキャスト文Ring r = (Ring) s
は許可されます。Ring
はfinal
クラスではないため、Shape
をRing
にできます。
シール・クラスおよびインタフェースに関連するAPI
クラスjava.lang.Classには、シール・クラスおよびインタフェースに関連する次の2つの新しいメソッドがあります:
- java.lang.constant.ClassDesc[] permittedSubclasses(): シールされている場合、クラスの許可されたすべてのサブクラスを表すjava.lang.constant.ClassDescオブジェクトを含む配列を返します。クラスがシールされていない場合は、空の配列を返します
- boolean isSealed(): 指定されたクラスまたはインタフェースがシールされている場合はtrueを返し、それ以外の場合はfalseを返します