W3docs

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 elenco permits.
  • non-sealed — rinuncia al sealing; chiunque può ora estendere Triangle. 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, sealed o non-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 scrivere final esplicitamente. 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

java— editable, runs on the server

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.

Esercitazione

Pratica
Cosa si ottiene dichiarando una classe `sealed`?
Cosa si ottiene dichiarando una classe `sealed`?
Was this page helpful?