Comment utiliser les classes scellées en Java 17 ?

Une énumération avec de nombreux constructeurs, champs et méthodes peut commencer à ressembler à une classe complète. Et si nous pouvions créer une classe mais limiter les sous-classes directes à un ensemble fixe de classes ? Voici les classes scellées ! Une classe scellée est une classe qui restreint quelles autres classes peuvent l’étendre directement. Ces classes sont nouvelles en Java 17.

Avez-vous remarqué que nous avons dit étendre directement dans la définition d’une classe scellée ? Comme vous le verrez bientôt, il existe un moyen pour une classe non nommée dans la déclaration de classe scellée de l’étendre indirectement. Sauf indication contraire, supposons que nous faisons référence aux sous-classes qui étendent directement la classe scellée.

Déclaration d’une Classe Scellée

Commençons par un exemple simple. Une classe scellée déclare une liste de classes qui peuvent l’étendre, tandis que les sous-classes déclarent qu’elles étendent la classe scellée. La Figure 7.5 déclare une classe scellée avec deux sous-classes directes.

Remarquez-vous quelque chose de nouveau ? Java 17 inclut trois nouveaux mots-clés que vous devriez connaître. Nous utilisons souvent final avec les sous-classes scellées, mais nous aborderons chacun d’eux après avoir couvert les bases.

Mots-clés des Classes Scellées

  • sealed : Indique qu’une classe ou une interface ne peut être étendue/implémentée que par des classes ou interfaces nommées
  • permits : Utilisé avec le mot-clé sealed pour lister les classes et interfaces autorisées
  • non-sealed : Appliqué à une classe ou interface qui étend une classe scellée, indiquant qu’elle peut être étendue par des classes non spécifiées

Plutôt facile jusqu’ici, n’est-ce pas ? Voyons pourquoi chacun de ces ensembles de déclarations ne compile pas :

public class sealed Grenouille permits GrenouilleVerte {} // NE COMPILE PAS
public final class GrenouilleVerte extends Grenouille {}

public abstract sealed class Loup permits Bois {}
public final class Bois extends Loup {}
public final class MonLoup extends Loup {} // NE COMPILE PAS

Le premier exemple ne compile pas parce que les modificateurs de classe et sealed sont dans le mauvais ordre. Le modificateur doit être placé avant le type de classe. Le deuxième exemple ne compile pas parce que MonLoup n’est pas listé dans la déclaration de Loup.

Les classes scellées sont souvent déclarées avec le modificateur abstract, bien que ce ne soit certainement pas obligatoire.

Déclarer une classe scellée avec le modificateur sealed est la partie facile. La plupart du temps, si vous voyez une question sur les classes scellées, elle teste votre connaissance de la façon dont la sous-classe étend correctement la classe scellée. Il y a un certain nombre de règles importantes que vous devez connaître, alors lisez attentivement les sections suivantes.

Compilation des Classes Scellées

Disons que nous créons une classe Pingouin et la compilons dans un nouveau package sans aucun autre code source. En gardant cela à l’esprit, est-ce que ce qui suit compile ?

// Pingouin.java
package zoo;
public sealed class Pingouin permits Empereur {}

Non, ce n’est pas le cas ! Pourquoi ? La réponse est qu’une classe scellée doit être déclarée (et compilée) dans le même package que ses sous-classes directes. Mais qu’en est-il des sous-classes elles-mêmes ? Elles doivent chacune étendre la classe scellée. Par exemple, ce qui suit ne compile pas.

// Pingouin.java
package zoo;
public sealed class Pingouin permits Empereur {} // NE COMPILE PAS

// Empereur.java
package zoo;
public final class Empereur {}

Même si la classe Empereur est déclarée, elle n’étend pas la classe Pingouin.

Il y a plus ! Dans les modules nommés, les classes scellées et leurs sous-classes directes peuvent se trouver dans différents packages, à condition qu’elles soient dans le même module nommé.

Spécification du Modificateur de Sous-classe

Alors que certains types, comme les interfaces, ont un certain nombre de modificateurs implicites, les classes scellées n’en ont pas. Chaque classe qui étend directement une classe scellée doit spécifier exactement l’un des trois modificateurs suivants : final, sealed, ou non-sealed. Souvenez-vous de cette règle !

Une Sous-classe final

Le premier modificateur que nous allons examiner qui peut être appliqué à une sous-classe directe d’une classe scellée est le modificateur final.

public sealed class Antilope permits Gazelle {}
public final class Gazelle extends Antilope {}
public class Georges extends Gazelle {} // NE COMPILE PAS

Comme pour une classe ordinaire, le modificateur final empêche la sous-classe Gazelle d’être étendue davantage.

Une Sous-classe sealed

Examinons maintenant un exemple utilisant le modificateur sealed :

public sealed class Mammifere permits Equin {}
public sealed class Equin extends Mammifere permits Zebre {}
public final class Zebre extends Equin {}

Le modificateur sealed appliqué à la sous-classe Equin signifie que les mêmes types de règles qui s’appliquaient à la classe parent Mammifere doivent être présentes. À savoir, Equin définit sa propre liste de sous-classes autorisées. Notez dans cet exemple que Zebre est une sous-classe indirecte de Mammifere mais n’est pas nommée dans la classe Mammifere.

Malgré l’autorisation de sous-classes indirectes non nommées dans Mammifere, la liste des classes qui peuvent hériter de Mammifere reste fixe. Si vous avez une référence à un objet Mammifere, ce doit être un Mammifere, un Equin ou un Zebre.

Une Sous-classe non-sealed

Le modificateur non-sealed est utilisé pour ouvrir une classe parent scellée à des sous-classes potentiellement inconnues. Modifions notre exemple précédent pour permettre à MonLoup de compiler sans modifier la déclaration de Loup :

public sealed class Loup permits Bois {}
public non-sealed class Bois extends Loup {}
public class MonLoup extends Bois {}

Dans cet exemple, nous sommes en mesure de créer une sous-classe indirecte de Loup, appelée MonLoup, non nommée dans la déclaration de Loup. Notez également que MonLoup n’est pas final, elle peut donc être étendue par n’importe quelle sous-classe, comme MonLoupPoilu.

public class MonLoupPoilu extends MonLoup {}

À première vue, cela peut sembler un peu contre-intuitif. Après tout, nous avons pu créer des sous-classes de Loup qui n’étaient pas déclarées dans Loup. Loup est-il toujours scellé ? Oui, mais c’est grâce au polymorphisme. Toute instance de MonLoup ou MonLoupPoilu est aussi une instance de Bois, qui est nommée dans la déclaration de Loup. Nous discutons du polymorphisme plus en détail vers la fin de ce chapitre.

Si vous êtes toujours inquiet d’ouvrir trop une classe scellée avec une sous-classe non-sealed, rappelez-vous que la personne qui écrit la classe scellée peut voir la déclaration de toutes les sous-classes directes au moment de la compilation. Elle peut décider si elle autorise ou non la sous-classe non-sealed à être prise en charge.

Omission de la Clause permits

Jusqu’à présent, tous les exemples que vous avez vus nécessitaient une clause permits lors de la déclaration d’une classe scellée, mais ce n’est pas toujours le cas. Imaginez que vous ayez un fichier Serpent.java avec deux classes de premier niveau définies à l’intérieur :

// Serpent.java
public sealed class Serpent permits Cobra {}
final class Cobra extends Serpent {}

Dans ce cas, la clause permits est optionnelle et peut être omise. Le mot-clé extends est toujours requis dans la sous-classe, cependant :

// Serpent.java
public sealed class Serpent {}
final class Cobra extends Serpent {}

Si ces classes étaient dans des fichiers séparés, ce code ne compilerait pas ! Cette règle s’applique également aux classes scellées avec des sous-classes imbriquées.

// Serpent.java
public sealed class Serpent {
    final class Cobra extends Serpent {}
}

Référencement des Sous-classes Imbriquées

Bien qu’il soit plus facile de lire le code si vous omettez la clause permits pour les sous-classes imbriquées, vous êtes libre de les nommer. Cependant, la syntaxe pourrait être différente de ce que vous attendez.

public sealed class Serpent permits Cobra { // NE COMPILE PAS
    final class Cobra extends Serpent {}
}

Ce code ne compile pas parce que Cobra nécessite une référence à l’espace de noms de Serpent. Ce qui suit résout ce problème :

public sealed class Serpent permits Serpent.Cobra {
    final class Cobra extends Serpent {}
}

Lorsque toutes vos sous-classes sont imbriquées, nous recommandons fortement d’omettre la classe permits.

Le tableau suivant est une référence pratique pour ces cas.

Emplacement des sous-classes directesClause permits
Dans un fichier différent de la classe scelléeObligatoire
Dans le même fichier que la classe scelléeAutorisée, mais pas obligatoire
Imbriquée à l’intérieur de la classe scelléeAutorisée, mais pas obligatoire

Scellement des Interfaces

En plus des classes, les interfaces peuvent également être scellées. L’idée est analogue aux classes, et beaucoup des mêmes règles s’appliquent. Par exemple, l’interface scellée doit apparaître dans le même package ou module nommé que les classes ou interfaces qui l’étendent ou l’implémentent directement.

Une caractéristique distincte d’une interface scellée est que la liste permits peut s’appliquer à une classe qui implémente l’interface ou à une interface qui étend l’interface.

// Interface scellée
public sealed interface Nage permits Canard, Cygne, Flotte {}

// Classes autorisées à implémenter l'interface scellée
public final class Canard implements Nage {}
public final class Cygne implements Nage {}

// Interface autorisée à étendre l'interface scellée
public non-sealed interface Flotte extends Nage {}

Qu’en est-il du modificateur appliqué aux interfaces qui étendent l’interface scellée ? Eh bien, rappelez-vous que les interfaces sont implicitement abstract et ne peuvent pas être marquées final. Pour cette raison, les interfaces qui étendent une interface scellée ne peuvent être marquées que sealed ou non-sealed. Elles ne peuvent pas être marquées final.

Révision des Règles des Classes Scellées

Chaque fois que vous voyez une classe scellée, portez une attention particulière à la déclaration de la sous-classe et aux modificateurs.

Règles des Classes Scellées

  • Les classes scellées sont déclarées avec les modificateurs sealed et permits.
  • Les classes scellées doivent être déclarées dans le même package ou module nommé que leurs sous-classes directes.
  • Les sous-classes directes des classes scellées doivent être marquées final, sealed, ou non-sealed.
  • La clause permits est optionnelle si la classe scellée et ses sous-classes directes sont déclarées dans le même fichier ou si les sous-classes sont imbriquées dans la classe scellée.
  • Les interfaces peuvent être scellées pour limiter les classes qui les implémentent ou les interfaces qui les étendent.

Scénario du Monde Réel

Pourquoi Avoir des Classes Scellées ?

Dans le chapitre “Prendre des Décisions”, vous avez appris les expressions switch et la correspondance de motifs. Imaginez si nous pouvions traiter une classe scellée comme une énumération dans une expression switch en appliquant la correspondance de motifs. Étant donné une classe scellée Poisson avec deux sous-classes directes, cela pourrait ressembler à ceci :

public void afficherNom(Poisson poisson) {
    System.out.println(switch(poisson) {
        case Truite t -> t.getNomTruite();
        case Bar b -> b.getNomBar();
    });
}

Si Poisson n’était pas scellé, l’expression switch nécessiterait une branche par défaut, ou le code ne compilerait pas. Puisqu’il est scellé, le compilateur connaît toutes les options ! La bonne nouvelle est que cette fonctionnalité est en route, mais la mauvaise nouvelle est qu’elle est encore en Preview dans Java 17 et pas officiellement publiée. Nous voulions juste vous donner une idée de la direction de certaines de ces nouvelles fonctionnalités.