Java15の封印されたクラスとインターフェース
1. 概要
Java SE 15 のリリースでは、プレビュー機能として封印されたクラス( JEP 360 )が導入されています。
この機能は、Javaでよりきめ細かい継承制御を有効にすることを目的としています。 シーリングにより、クラスとインターフェースは許可されたサブタイプを定義できます。
つまり、クラスまたはインターフェイスは、どのクラスがそれを実装または拡張できるかを定義できるようになりました。 これは、ドメインモデリングとライブラリのセキュリティを強化するための便利な機能です。
2. 動機
クラス階層により、継承を介してコードを再利用できます。 ただし、クラス階層には他の目的もあります。 コードの再利用は素晴らしいですが、常に私たちの主な目標ではありません。
2.1. モデリングの可能性
クラス階層の別の目的は、ドメインに存在するさまざまな可能性をモデル化することです。
例として、オートバイではなく、車とトラックでのみ機能するビジネスドメインを想像してみてください。 JavaでVehicle抽象クラスを作成する場合、CarクラスとTruckクラスのみがそれを拡張できるようにする必要があります。 このようにして、ドメイン内でVehicle抽象クラスが誤用されないようにします。
この例では、すべての未知のサブクラスに対して防御するよりも、既知のサブクラスを処理するコードの明確さに関心があります。
バージョン15より前では、Javaはコードの再利用が常に目標であると想定していました。 すべてのクラスは、任意の数のサブクラスによって拡張可能でした。
2.2. パッケージ-プライベートアプローチ
以前のバージョンでは、Javaは継承制御の領域で制限されたオプションを提供していました。
最終クラスはサブクラスを持つことができません。 package-private class は、同じパッケージ内のサブクラスのみを持つことができます。
package-privateアプローチを使用すると、ユーザーは抽象クラスを拡張することを許可せずに抽象クラスにアクセスすることはできません。
public class Vehicles {
abstract static class Vehicle {
private final String registrationNumber;
public Vehicle(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public String getRegistrationNumber() {
return registrationNumber;
}
}
public static final class Car extends Vehicle {
private final int numberOfSeats;
public Car(int numberOfSeats, String registrationNumber) {
super(registrationNumber);
this.numberOfSeats = numberOfSeats;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
}
public static final class Truck extends Vehicle {
private final int loadCapacity;
public Truck(int loadCapacity, String registrationNumber) {
super(registrationNumber);
this.loadCapacity = loadCapacity;
}
public int getLoadCapacity() {
return loadCapacity;
}
}
}
2.3. スーパークラスアクセス可能、拡張不可
サブクラスのセットを使用して開発されたスーパークラスは、サブクラスを制約するのではなく、意図された使用法を文書化できる必要があります。 また、サブクラスを制限しても、そのスーパークラスのアクセス可能性が制限されることはありません。
したがって、封印されたクラスの背後にある主な動機は、スーパークラスが広くアクセス可能であるが、広く拡張可能ではない可能性を持つことです。
3. 創造
封印された機能は、Javaにいくつかの新しい修飾子と句を導入します:封印された、封印されていない、および許可。
3.1. 密閉されたインターフェース
インターフェイスをシールするために、sealed修飾子をその宣言に適用できます。 次に、 permit 句は、封印されたインターフェイスの実装を許可されるクラスを指定します。
public sealed interface Service permits Car, Truck {
int getMaxServiceIntervalInMonths();
default int getMaxDistanceBetweenServicesInKilometers() {
return 100000;
}
}
3.2. 封印されたクラス
インターフェイスと同様に、同じSealed修飾子を適用することでクラスを封印できます。 permit 句は、extendsまたはimplements句の後に定義する必要があります。
public abstract sealed class Vehicle permits Car, Truck {
protected final String registrationNumber;
public Vehicle(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public String getRegistrationNumber() {
return registrationNumber;
}
}
許可されるサブクラスは、修飾子を定義する必要があります。 それ以上の拡張を防ぐために、最終と宣言される場合があります。
public final class Truck extends Vehicle implements Service {
private final int loadCapacity;
public Truck(int loadCapacity, String registrationNumber) {
super(registrationNumber);
this.loadCapacity = loadCapacity;
}
public int getLoadCapacity() {
return loadCapacity;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 18;
}
}
許可されたサブクラスは、封印されたと宣言することもできます。 ただし、封印されていない、と宣言すると、拡張可能になります。
public non-sealed class Car extends Vehicle implements Service {
private final int numberOfSeats;
public Car(int numberOfSeats, String registrationNumber) {
super(registrationNumber);
this.numberOfSeats = numberOfSeats;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 12;
}
}
3.4. 制約
封印されたクラスは、許可されたサブクラスに3つの重要な制約を課します。
- 許可されるすべてのサブクラスは、封印されたクラスと同じモジュールに属している必要があります。
- 許可されたすべてのサブクラスは、封印されたクラスを明示的に拡張する必要があります。
- 許可されるすべてのサブクラスは、修飾子を定義する必要があります: final 、 Sealed 、またはnon-sealed。
4. 使用法
4.1. 伝統的な方法
クラスを封印するとき、クライアントコードが許可されたすべてのサブクラスについて明確に推論できるようにします。
サブクラスについて推論する従来の方法は、一連のif-elseステートメントとinstanceofチェックを使用することです。
if (vehicle instanceof Car) {
return ((Car) vehicle).getNumberOfSeats();
} else if (vehicle instanceof Truck) {
return ((Truck) vehicle).getLoadCapacity();
} else {
throw new RuntimeException("Unknown instance of Vehicle");
}
4.2. パターンマッチング
パターンマッチングを適用することで、追加のクラスキャストを回避できますが、i f-elseステートメントのセットが必要です。
if (vehicle instanceof Car car) {
return car.getNumberOfSeats();
} else if (vehicle instanceof Truck truck) {
return truck.getLoadCapacity();
} else {
throw new RuntimeException("Unknown instance of Vehicle");
}
i f-else を使用すると、許可されているすべてのサブクラスをカバーしているとコンパイラーが判断するのが困難になります。 そのため、RuntimeExceptionをスローしています。
Javaの将来のバージョンでは、クライアントコードはi f-else ( JEP 375 )の代わりにswitchステートメントを使用できるようになります。
タイプのテストパターンを使用することにより、コンパイラーは、許可されたすべてのサブクラスがカバーされていることを確認できます。 したがって、default句/ケースは不要になります。
4. 互換性
次に、封印されたクラスと、レコードやリフレクションAPIなどの他のJava言語機能との互換性を見てみましょう。
4.1. 記録
封印されたクラスは、レコードで非常にうまく機能します。 レコードは暗黙的に最終的なものであるため、封印された階層はさらに簡潔になります。 レコードを使用してクラスの例を書き直してみましょう。
public sealed interface Vehicle permits Car, Truck {
String getRegistrationNumber();
}
public record Car(int numberOfSeats, String registrationNumber) implements Vehicle {
@Override
public String getRegistrationNumber() {
return registrationNumber;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
}
public record Truck(int loadCapacity, String registrationNumber) implements Vehicle {
@Override
public String getRegistrationNumber() {
return registrationNumber;
}
public int getLoadCapacity() {
return loadCapacity;
}
}
4.2. 反射
封印されたクラスは、リフレクションAPI でもサポートされており、 java.lang.Class:に2つのパブリックメソッドが追加されています。
- isSealed メソッドは、指定されたクラスまたはインターフェイスがシールされている場合、trueを返します。
- メソッドpermittedSubclassesは、許可されたすべてのサブクラスを表すオブジェクトの配列を返します。
これらのメソッドを使用して、次の例に基づくアサーションを作成できます。
Assertions.assertThat(truck.getClass().isSealed()).isEqualTo(false);
Assertions.assertThat(truck.getClass().getSuperclass().isSealed()).isEqualTo(true);
Assertions.assertThat(truck.getClass().getSuperclass().permittedSubclasses())
.contains(ClassDesc.of(truck.getClass().getCanonicalName()));
5. 結論
この記事では、JavaSE15のプレビュー機能である封印されたクラスとインターフェースについて説明しました。 封印されたクラスとインターフェースの作成と使用法、およびそれらの制約と他の言語機能との互換性について説明しました。
例では、封印されたインターフェースと封印されたクラスの作成、封印されたクラスの使用(パターンマッチングありとなし)、およびレコードとリフレクションAPIとの封印されたクラスの互換性について説明しました。
いつものように、完全なソースコードはGitHubでから入手できます。