Classi e interfacce sigillate in Java 15

1. Panoramica

Il rilascio di Java SE 15 introduce le classi sealed (JEP 360) come funzionalità di anteprima.

Questa funzione riguarda l'abilitazione di un controllo dell'ereditarietà più dettagliato in Java. Il sigillamento consente alle classi e alle interfacce di definire i sottotipi consentiti.

In altre parole, una classe o un'interfaccia può ora definire quali classi possono implementarla o estenderla. È una funzionalità utile per la modellazione dei domini e per aumentare la sicurezza delle biblioteche.

2. Motivazione

Una gerarchia di classi ci consente di riutilizzare il codice tramite l'ereditarietà. Tuttavia, la gerarchia di classi può avere anche altri scopi. Il riutilizzo del codice è ottimo, ma non è sempre il nostro obiettivo principale.

2.1. Possibilità di modellazione

Uno scopo alternativo di una gerarchia di classi può essere quello di modellare varie possibilità che esistono in un dominio.

Ad esempio, immagina un dominio aziendale che funziona solo con auto e camion, non motocicli. Quando si crea la classe astratta Vehicle in Java, dovremmo essere in grado di consentire solo alle classi Car e Truck di estenderla. In questo modo, vogliamo assicurarci che non ci sarà un uso improprio della classe astratta Vehicle nel nostro dominio.

In questo esempio, siamo più interessati alla chiarezza del codice che gestisce le sottoclassi note che alla difesa contro tutte le sottoclassi sconosciute .

Prima della versione 15, Java pensava che il riutilizzo del codice fosse sempre un obiettivo. Ogni classe era estendibile da qualsiasi numero di sottoclassi.

2.2. L'approccio pacchetto privato

Nelle versioni precedenti, Java forniva opzioni limitate nell'area del controllo dell'ereditarietà.

Una classe finale non può avere sottoclassi. Una classe privata del pacchetto può avere solo sottoclassi nello stesso pacchetto.

Utilizzando l'approccio del pacchetto privato, gli utenti non possono accedere alla classe astratta senza consentire loro di estenderla:

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. Accessibile alla superclasse, non estensibile

Una superclasse sviluppata con un insieme delle sue sottoclassi dovrebbe essere in grado di documentare il suo utilizzo previsto, non vincolare le sue sottoclassi. Inoltre, avere sottoclassi limitate non dovrebbe limitare l'accessibilità della sua superclasse.

Pertanto, la motivazione principale dietro le classi sealed è avere la possibilità che una superclasse sia ampiamente accessibile ma non ampiamente estensibile.

3. Creazione

La funzionalità sealed introduce un paio di nuovi modificatori e clausole in Java: sealed, non sealed e permits .

3.1. Interfacce sigillate

Per sigillare un'interfaccia, possiamo applicare il modificatore sigillato alla sua dichiarazione. Il permessi clausola quindi specifica le classi che sono consentiti per implementare l'interfaccia sigillato:

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

3.2. Classi sigillate

Simile alle interfacce, possiamo sigillare le classi applicando lo stesso modificatore sigillato . La clausola di autorizzazione dovrebbe essere definita dopo qualsiasi clausola di estensione o implementazione :

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

Una sottoclasse consentita deve definire un modificatore. Può essere dichiarato definitivo per evitare ulteriori proroghe:

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

Una sottoclasse consentita può anche essere dichiarata sigillata . Tuttavia, se lo dichiariamo non sigillato, è aperto per l'estensione:

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

Una classe sigillata impone tre importanti vincoli alle sue sottoclassi consentite:

  1. Tutte le sottoclassi consentite devono appartenere allo stesso modulo della classe sealed.
  2. Ogni sottoclasse consentita deve estendere esplicitamente la classe sealed.
  3. Ogni sottoclasse consentita deve definire un modificatore: final , sealed o non-sealed.

4. Utilizzo

4.1. Il modo tradizionale

Quando si sigilla una classe, consentiamo al codice client di ragionare chiaramente su tutte le sottoclassi consentite.

Il modo tradizionale di ragionare sulla sottoclasse è usare un insieme di istruzioni if-else e istanze di controlli:

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. Corrispondenza del modello

Applicando il pattern matching, possiamo evitare il cast aggiuntivo della classe, ma abbiamo ancora bisogno di un insieme di istruzioni 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

In questo articolo, abbiamo esplorato le classi e le interfacce sigillate, una funzionalità di anteprima in Java SE 15. Abbiamo coperto la creazione e l'utilizzo di classi e interfacce sigillate, nonché i loro vincoli e la compatibilità con altre funzionalità del linguaggio.

Negli esempi, abbiamo coperto la creazione di un'interfaccia sigillata e di una classe sigillata, l'uso della classe sigillata (con e senza corrispondenza di modelli) e la compatibilità delle classi sigillate con i record e l'API di riflessione.

Come sempre, il codice sorgente completo è disponibile su GitHub.