Versiegelte Klassen und Schnittstellen in Java 15

1. Übersicht

Mit der Veröffentlichung von Java SE 15 werden versiegelte Klassen (JEP 360) als Vorschaufunktion eingeführt.

Bei dieser Funktion geht es darum, eine detailliertere Vererbungskontrolle in Java zu ermöglichen. Durch das Versiegeln können Klassen und Schnittstellen ihre zulässigen Untertypen definieren.

Mit anderen Worten, eine Klasse oder eine Schnittstelle kann jetzt definieren, welche Klassen sie implementieren oder erweitern können. Es ist eine nützliche Funktion zur Domänenmodellierung und zur Erhöhung der Sicherheit von Bibliotheken.

2. Motivation

Eine Klassenhierarchie ermöglicht es uns, Code über Vererbung wiederzuverwenden. Die Klassenhierarchie kann jedoch auch andere Zwecke haben. Die Wiederverwendung von Code ist großartig, aber nicht immer unser primäres Ziel.

2.1. Modellierungsmöglichkeiten

Ein alternativer Zweck einer Klassenhierarchie kann darin bestehen, verschiedene Möglichkeiten zu modellieren, die in einer Domäne existieren.

Stellen Sie sich als Beispiel eine Geschäftsdomäne vor, die nur mit Autos und Lastwagen funktioniert, nicht mit Motorrädern. Wenn Sie die abstrakte Fahrzeugklasse in Java erstellen , sollten wir nur die Klassen Auto und LKW zulassen können, um sie zu erweitern. Auf diese Weise möchten wir sicherstellen, dass die abstrakte Fahrzeugklasse in unserer Domäne nicht missbraucht wird .

In diesem Beispiel interessiert uns mehr die Klarheit der Code-Behandlung bekannter Unterklassen als die Verteidigung gegen alle unbekannten Unterklassen .

Vor Version 15 ging Java davon aus, dass die Wiederverwendung von Code immer ein Ziel ist. Jede Klasse war um eine beliebige Anzahl von Unterklassen erweiterbar.

2.2. Der paketprivate Ansatz

In früheren Versionen bot Java begrenzte Optionen im Bereich der Vererbungskontrolle.

Eine Abschlussklasse kann keine Unterklassen haben. Eine paketprivate Klasse kann nur Unterklassen im selben Paket haben.

Mit dem paketprivaten Ansatz können Benutzer nicht auf die abstrakte Klasse zugreifen, ohne sie auch erweitern zu können:

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. Superklasse zugänglich, nicht erweiterbar

Eine Superklasse, die mit einer Reihe ihrer Unterklassen entwickelt wird, sollte in der Lage sein, ihre beabsichtigte Verwendung zu dokumentieren, ohne ihre Unterklassen einzuschränken. Eingeschränkte Unterklassen sollten auch die Zugänglichkeit ihrer Oberklasse nicht einschränken.

Daher besteht die Hauptmotivation hinter versiegelten Klassen darin, die Möglichkeit zu haben, dass eine Oberklasse allgemein zugänglich, aber nicht allgemein erweiterbar ist.

3. Schöpfung

Die versiegelte Funktion führt einige neue Modifikatoren und Klauseln in Java ein: versiegelt, nicht versiegelt und erlaubt .

3.1. Versiegelte Schnittstellen

Um eine Schnittstelle abzudichten, können wir den versiegelten Modifikator auf seine Deklaration anwenden . Die Genehmigungsklausel gibt dann die Klassen an, die die versiegelte Schnittstelle implementieren dürfen:

public sealed interface Service permits Car, Truck { int getMaxServiceIntervalInMonths(); default int getMaxDistanceBetweenServicesInKilometers() { return 100000; } }

3.2. Versiegelte Klassen

Ähnlich wie bei Schnittstellen können wir Klassen versiegeln, indem wir denselben versiegelten Modifikator anwenden . Die Genehmigungsklausel sollte definiert werden, nachdem Klauseln erweitert oder implementiert wurden :

public abstract sealed class Vehicle permits Car, Truck { protected final String registrationNumber; public Vehicle(String registrationNumber) { this.registrationNumber = registrationNumber; } public String getRegistrationNumber() { return registrationNumber; } }

Eine zulässige Unterklasse muss einen Modifikator definieren. Es kann für endgültig erklärt werden , um weitere Erweiterungen zu verhindern:

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; } }

Eine zulässige Unterklasse kann auch als versiegelt deklariert werden . Wenn wir es jedoch für nicht versiegelt erklären, kann es erweitert werden:

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. Einschränkungen

Eine versiegelte Klasse unterwirft ihren zulässigen Unterklassen drei wichtige Einschränkungen:

  1. Alle zulässigen Unterklassen müssen zum selben Modul gehören wie die versiegelte Klasse.
  2. Jede zulässige Unterklasse muss die versiegelte Klasse explizit erweitern.
  3. Jede zulässige Unterklasse muss einen Modifikator definieren: endgültig , versiegelt oder nicht versiegelt.

4. Verwendung

4.1. Der traditionelle Weg

Beim Versiegeln einer Klasse ermöglichen wir dem Client-Code, alle zulässigen Unterklassen klar zu begründen.

Die traditionelle Art, über Unterklassen nachzudenken, besteht darin, eine Reihe von if-else- Anweisungen und Instanzen von Prüfungen zu verwenden:

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. Mustervergleich

Durch Anwenden des Mustervergleichs können wir die zusätzliche Klassenumwandlung vermeiden, benötigen jedoch noch eine Reihe von i f-else- Anweisungen:

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"); }

Using if-else makes it difficult for the compiler to determine that we covered all permitted subclasses. For that reason, we are throwing a RuntimeException.

In future versions of Java, the client code will be able to use a switch statement instead of if-else (JEP 375).

By using type test patterns, the compiler will be able to check that every permitted subclass is covered. Thus, there will be no more need for a default clause/case.

4. Compatibility

Let's now take a look at the compatibility of sealed classes with other Java language features like records and the reflection API.

4.1. Records

Sealed classes work very well with records. Since records are implicitly final, the sealed hierarchy is even more concise. Let's try to rewrite our class example using records:

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. Reflection

Sealed classes are also supported by the reflection API, where two public methods have been added to the java.lang.Class:

  • The isSealed method returns true if the given class or interface is sealed.
  • Method permittedSubclasses returns an array of objects representing all the permitted subclasses.

We can make use of these methods to create assertions that are based on our example:

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. Conclusion

In diesem Artikel haben wir versiegelte Klassen und Schnittstellen untersucht, eine Vorschaufunktion in Java SE 15. Wir haben die Erstellung und Verwendung versiegelter Klassen und Schnittstellen sowie deren Einschränkungen und Kompatibilität mit anderen Sprachfunktionen behandelt.

In den Beispielen haben wir die Erstellung einer versiegelten Schnittstelle und einer versiegelten Klasse, die Verwendung der versiegelten Klasse (mit und ohne Mustervergleich) sowie die Kompatibilität versiegelter Klassen mit Datensätzen und der Reflection-API behandelt.

Wie immer ist der vollständige Quellcode auf GitHub verfügbar.