Classi Sealed in Java
Limita quali classi possono estendere o implementare un tipo in Java usando le classi sealed e la clausola permits.
Una classe o interfaccia sealed limita chi può estenderla o implementarla a un elenco fisso e nominato di sottotipi. final dice "nessuno può estendermi." sealed dice "solo queste classi specifiche possono." Ti offre una gerarchia chiusa — il compilatore conosce l'intera famiglia in anticipo, il che sblocca switch esaustivi e una modellazione disciplinata di forme "uno tra N".
Senza il sealing, un abstract class Shape è aperto al mondo: chiunque abbia accesso al tipo può scrivere class Banana extends Shape. Con sealed, l'autore di Shape dichiara esattamente quali sottotipi esistono, e aggiungerne uno richiede la modifica del genitore.
La sintassi di base
Una classe sealed elenca i sottotipi permessi con permits:
public sealed class Shape
permits Circle, Square, Triangle {
// common state and behavior
}Ogni sottotipo permesso deve a sua volta dichiarare cosa fa con il seal — uno tra final, sealed (con i propri permits) o non-sealed:
public final class Circle extends Shape { /* leaf */ }
public final class Square extends Shape { /* leaf */ }
public non-sealed class Triangle extends Shape { /* re-opens the door */ }final— nessuna ulteriore sottoclasse; questo è un nodo foglia nella gerarchia.sealed— estende lo stesso modello; ha il proprio elencopermits.non-sealed— rinuncia al sealing; chiunque può ora estendereTriangle. Utile quando vuoi una famiglia chiusa di primo livello con un ramo aperto.
Un tipo sealed senza modificatore su un sottotipo è un errore di compilazione — il compilatore ti obbliga a scegliere.
Interfacce sealed
Le interfacce seguono le stesse regole e sono solitamente la scelta più naturale per modellare famiglie di casi:
public sealed interface Result<T>
permits Success, Failure {}
public record Success<T>(T value) implements Result<T> {}
public record Failure<T>(String message) implements Result<T> {}Combinate con i record, ottieni qualcosa di simile al "sum type" o "tagged union" dei linguaggi funzionali — un elenco chiuso di alternative nominate, ciascuna con i propri dati.
Stesso modulo, stesso package (o permits espliciti)
I sottotipi permessi devono essere accessibili alla dichiarazione sealed al momento della compilazione. La soluzione più semplice è inserire la classe sealed e i suoi sottotipi permessi nello stesso file sorgente — in tal caso puoi persino omettere permits, poiché il compilatore lo inferisce:
public sealed interface Tree {
record Leaf(int value) implements Tree {}
record Node(Tree left, Tree right) implements Tree {}
}Se si trovano in file separati, devono essere nello stesso package (o, in un progetto modulare, nello stesso modulo), e la clausola permits è obbligatoria.
Il vantaggio: switch esaustivo
Il compilatore conosce ogni possibile sottotipo di un tipo sealed. Questo consente a switch di imporre l'esaustività senza un default:
double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Square q -> q.side() * q.side();
case Triangle t -> 0.5 * t.base() * t.height();
};
}Se in seguito aggiungi un Hexagon permesso, questo switch smette di compilare ovunque appaia finché non gestisci il nuovo caso. Questa è esattamente la rete di sicurezza che default distruggerebbe silenziosamente.
Regole imposte dal compilatore
Alcune vincoli sono facili da ignorare:
- Ogni sottotipo permesso deve estendere o implementare direttamente il tipo sealed. Non puoi elencare un nipote in
permits— solo i sottotipi immediati. - Ogni sottotipo permesso deve scegliere un modificatore:
final,sealedonon-sealed. Dimenticarlo è un errore di compilazione. - I tipi permessi devono essere trovabili al momento della compilazione — stesso file, stesso package o lo stesso modulo nominato. Un tipo sealed non può permettere una classe in un modulo non correlato.
- I record sono implicitamente
final, quindi un record può essere un sottotipo permesso senza scriverefinalesplicitamente. Ecco perché la combinazione sealed-interface-più-record è così elegante.
Il modificatore non-sealed è la valvola di sfogo deliberata. Usalo quando la maggior parte di una gerarchia deve rimanere chiusa ma un ramo è un punto di estensione intenzionale:
public sealed interface Vehicle permits Car, Truck, CustomBuild {}
public record Car(int doors) implements Vehicle {}
public record Truck(double tons) implements Vehicle {}
// Re-opened: third parties may extend this branch.
public non-sealed interface CustomBuild extends Vehicle {}Poiché CustomBuild è non-sealed, uno switch su Vehicle necessita ancora di un fallback per esso — il compilatore non può più dimostrare che quel ramo è esaustivo.
Quando usare il sealing
Ricorri al sealing quando l'astrazione è genuinamente un insieme chiuso di casi:
- Nodi AST o di espressioni (
Literal,Add,Multiply...). - Risultati di dominio che sono "successo o uno di questi errori."
- Gerarchie di comandi/eventi dove ogni consumatore a valle deve gestire ogni caso.
Non applicare il sealing a tipi che sono punti di estensione — interfacce plugin, hook di framework, qualsiasi cosa che i chiamanti dovrebbero estendere. Farlo vanificherebbe il loro scopo.
Un esempio pratico
Cosa viene dopo
Il sealing blocca l'elenco dei sottotipi. Il prossimo capitolo riguarda il chiedere, a runtime, quale sottotipo hai effettivamente — l'operatore instanceof e la sua forma moderna di pattern matching, che è ciò che rende lo switch sopra così conciso. Continua su Operatore instanceof in Java.