Classes et interfaces scellées dans Java 15

1. Vue d'ensemble

La version de Java SE 15 introduit les classes scellées (JEP 360) en tant que fonctionnalité de prévisualisation.

Cette fonctionnalité permet d'activer un contrôle d'héritage plus fin en Java. Le scellement permet aux classes et aux interfaces de définir leurs sous-types autorisés.

En d'autres termes, une classe ou une interface peut désormais définir quelles classes peuvent l'implémenter ou l'étendre. C'est une fonctionnalité utile pour la modélisation de domaine et pour augmenter la sécurité des bibliothèques.

2. Motivation

Une hiérarchie de classes nous permet de réutiliser du code via l'héritage. Cependant, la hiérarchie des classes peut également avoir d'autres objectifs. La réutilisation du code est excellente mais n'est pas toujours notre objectif principal.

2.1. Possibilités de modélisation

Un autre objectif d'une hiérarchie de classes peut être de modéliser diverses possibilités qui existent dans un domaine.

À titre d'exemple, imaginez un domaine d'activité qui ne fonctionne qu'avec des voitures et des camions, pas des motos. Lors de la création de la classe abstraite Vehicle en Java, nous devrions pouvoir autoriser uniquement les classes Car et Truck à l'étendre. De cette manière, nous voulons nous assurer qu'il n'y aura pas d'utilisation abusive de la classe abstraite Vehicle dans notre domaine.

Dans cet exemple, nous sommes plus intéressés par la clarté du code de gestion des sous-classes connues que par la défense contre toutes les sous-classes inconnues .

Avant la version 15, Java supposait que la réutilisation du code était toujours un objectif. Chaque classe était extensible par n'importe quel nombre de sous-classes.

2.2. L'approche forfaitaire privée

Dans les versions antérieures, Java offrait des options limitées dans le domaine du contrôle d'héritage.

Une classe finale ne peut avoir aucune sous-classe. Une classe privée de package ne peut avoir que des sous-classes dans le même package.

En utilisant l'approche package-private, les utilisateurs ne peuvent pas accéder à la classe abstraite sans leur permettre également de l'étendre:

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. Superclasse accessible, non extensible

Une superclasse qui est développée avec un ensemble de ses sous-classes devrait être capable de documenter son utilisation prévue, pas de contraindre ses sous-classes. De plus, avoir des sous-classes restreintes ne devrait pas limiter l'accessibilité de sa superclasse.

Ainsi, la principale motivation derrière les classes scellées est d'avoir la possibilité pour une superclasse d'être largement accessible mais pas largement extensible.

3. Création

La fonctionnalité scellée introduit quelques nouveaux modificateurs et clauses en Java: scellés, non scellés et permis .

3.1. Interfaces scellées

Pour sceller une interface, nous pouvons appliquer le modificateur scellé à sa déclaration. La clause permit spécifie ensuite les classes autorisées à implémenter l'interface scellée:

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

3.2. Classes scellées

Semblable aux interfaces, nous pouvons sceller les classes en appliquant le même modificateur scellé . La clause permit doit être définie après toute clause extend ou 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; } }

Une sous-classe autorisée doit définir un modificateur. Il peut être déclaré définitif pour éviter toute extension ultérieure:

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

Une sous-classe autorisée peut également être déclarée scellée . Cependant, si nous le déclarons non scellé, alors il est ouvert à l'extension:

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

Une classe scellée impose trois contraintes importantes à ses sous-classes autorisées:

  1. Toutes les sous-classes autorisées doivent appartenir au même module que la classe scellée.
  2. Chaque sous-classe autorisée doit étendre explicitement la classe scellée.
  3. Chaque sous-classe autorisée doit définir un modificateur: final , scellé ou non scellé.

4. Utilisation

4.1. La manière traditionnelle

Lors du scellement d'une classe, nous permettons au code client de raisonner clairement sur toutes les sous-classes autorisées.

La manière traditionnelle de raisonner à propos de la sous-classe consiste à utiliser un ensemble d' instructions if-else et d' instances de vérifications:

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. Correspondance de motif

En appliquant la correspondance de modèle, nous pouvons éviter le cast de classe supplémentaire, mais nous avons toujours besoin d'un ensemble d' instructions 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"); }

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

Dans cet article, nous avons exploré les classes et interfaces scellées, une fonctionnalité de prévisualisation de Java SE 15. Nous avons couvert la création et l'utilisation de classes et d'interfaces scellées, ainsi que leurs contraintes et leur compatibilité avec d'autres fonctionnalités du langage.

Dans les exemples, nous avons couvert la création d'une interface scellée et d'une classe scellée, l'utilisation de la classe scellée (avec et sans correspondance de modèle) et la compatibilité des classes scellées avec les enregistrements et l'API de réflexion.

Comme toujours, le code source complet est disponible à l'adresse over sur GitHub.