W3docs

Java Sealed Types In Depth

Modella gerarchie di tipi chiuse in Java con classi e interfacce sealed, in combinazione con il pattern matching.

Le classi e le interfacce sealed (introdotte definitivamente in Java 17) permettono a un tipo di dichiarare esattamente quali altri tipi possono estenderlo o implementarlo. Invece di una gerarchia aperta che chiunque può estendere, si definisce un insieme chiuso su cui il compilatore può ragionare. Questa singola garanzia — questi e solo questi — è ciò che rende possibile il pattern matching esaustivo e rende sicuro modellare gerarchie di classi orientate ai dati.

Un tipo sealed è il complemento naturale dei record. I record forniscono i dati; il sealing fornisce l'insieme chiuso di casi. Insieme portano i tipi di dati algebrici (il "tipo somma" che potresti conoscere da Kotlin, Rust o Scala) nel normale Java, e cambiano il comportamento di uno switch su una gerarchia.

Questo capitolo spiega come chiudere un tipo con permits, la scelta obbligatoria tra final, sealed e non-sealed che ogni sottotipo deve effettuare, perché una gerarchia chiusa abilita uno switch esaustivo senza default, e come il sealing si combina con la decostruzione dei record e i pattern con guardie. Si basa su interfacce e ereditarietà.

Chiudere un tipo con permits

Un tipo diventa sealed con il modificatore sealed e una clausola permits che elenca ogni sottotipo diretto. Nessun'altra classe può unirsi alla gerarchia, nemmeno nello stesso package. I sottotipi permessi devono essere accessibili al tipo sealed e, nel modulo senza nome, risiedere nello stesso package (o nello stesso modulo).

public sealed interface Payment
        permits Cash, Card, BankTransfer {}

public record Cash(int amount) implements Payment {}
public record Card(String number, int amount) implements Payment {}
public record BankTransfer(String iban, int amount) implements Payment {}

Se un sottotipo si trova nello stesso file sorgente, la clausola permits è opzionale — il compilatore la inferisce dal file. È necessario specificare permits esplicitamente solo quando i sottotipi si trovano in file separati.

// Same file: permits is inferred, so it can be omitted.
sealed interface Expr {
    record Num(int value) implements Expr {}
    record Add(Expr left, Expr right) implements Expr {}
}

La regola final, sealed e non-sealed

Ogni sottotipo permesso deve dichiarare come viene chiusa la propria parte della gerarchia. Il compilatore impone una scelta: ogni sottotipo diretto deve essere dichiarato final, sealed o non-sealed. Non esiste un'opzione "non fare nulla" — omettere il modificatore è un errore di compilazione.

ModificatoreSignificato per il sottotipo
finalIl sottotipo non può essere ulteriormente esteso. I record sono implicitamente final.
sealedIl sottotipo è anch'esso chiuso e fornisce la propria lista permits.
non-sealedIl sottotipo riapre la gerarchia — chiunque può di nuovo estenderlo.
public sealed class Shape permits Circle, Polygon, Freeform {}

public final class Circle extends Shape {}          // closed here
public sealed class Polygon extends Shape            // closed, but to a set
        permits Triangle, Rectangle {}
public non-sealed class Freeform extends Shape {}    // reopened: any subclass allowed

public final class Triangle extends Polygon {}
public final class Rectangle extends Polygon {}

non-sealed è la valvola di sfogo: permette a un ramo di una gerarchia altrimenti chiusa di restare aperto all'estensione. Usarlo con parsimonia, perché rinuncia alla garanzia di esaustività per quel ramo.

Perché il sealing abilita lo switch esaustivo

Il vantaggio di chiudere una gerarchia è che il compilatore conosce la lista completa dei casi. Uno switch su un tipo sealed che copre ogni sottotipo permesso è esaustivo, quindi non si scrive un ramo default. Ancora meglio: se in seguito viene aggiunto un nuovo sottotipo permesso, ogni switch non esaustivo smette di compilare — il compilatore indica il codice che ha dimenticato il nuovo caso.

sealed interface Payment permits Cash, Card, BankTransfer {}
record Cash(int amount) implements Payment {}
record Card(String number, int amount) implements Payment {}
record BankTransfer(String iban, int amount) implements Payment {}

static String fee(Payment p) {
    return switch (p) {                 // no default needed
        case Cash c         -> "no fee";
        case Card c         -> "2% card fee";
        case BankTransfer b -> "flat fee";
    };
}

Rimuovere il caso BankTransfer farà sì che il codice non compili: "the switch expression does not cover all possible input values." Questa segnalazione a tempo di compilazione è la ragione principale per chiudere una gerarchia.

Record, decostruzione e guardie

Poiché i sottotipi permessi sono solitamente record, è possibile combinare il sealing con i pattern di decostruzione dei record e i pattern con guardie (when) — vedere pattern matching per la funzionalità completa. La decostruzione associa i componenti del record direttamente nell'etichetta case; una guardia aggiunge una condizione booleana. L'ordine è importante: i casi con guardie più specifici devono precedere il fallback senza guardia per lo stesso tipo.

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

static String describe(Shape s) {
    return switch (s) {
        case Circle(double r) when r > 10        -> "big circle";
        case Circle(double r)                    -> "circle r=" + r;
        case Rectangle(double w, double h) when w == h -> "square";
        case Rectangle(double w, double h)       -> "rectangle";
    };
}

Il compilatore considera ancora questo switch esaustivo: ogni sottotipo permesso è abbinato da almeno un'etichetta senza guardia, quindi lo switch è totale anche se alcune etichette hanno guardie.

Un esempio completo

L'esempio eseguibile qui sotto mette insieme tutto: un'interfaccia sealed Shape con tre sottotipi record, uno switch esaustivo per l'area, uno switch con decostruzione e guardie per la descrizione, e uno sguardo ai metadati di sealing tramite reflection. Usa solo il JDK, quindi può essere eseguito così com'è.

java— editable, runs on the server

Cosa osservare dall'esecuzione:

  • Lo switch dell'area non ha un ramo default — poiché Shape è sealed, coprire tutti e tre i record è già esaustivo.
  • describe stampa big circle r=12.0 solo per il cerchio di raggio 12, dimostrando che la guardia when r > 10 viene valutata prima dell'etichetta Circle senza guardia.
  • Il rettangolo con lato 5 stampa square side=5.0, mostrando che la guardia w == h prevale sul caso Rectangle semplice che la segue.
  • L'area totale (525.96) viene accumulata su ogni sottotipo record, confermando che un singolo ciclo polimorfico gestisce l'intera gerarchia chiusa.
  • Shape.class.isSealed() restituisce true e getPermittedSubclasses() elenca Circle, Rectangle e Triangle — l'insieme permits sopravvive nei metadati a runtime.

Esercitazione

Pratica
Perché uno switch esaustivo su un'interfaccia sealed può omettere il ramo default?
Perché uno switch esaustivo su un'interfaccia sealed può omettere il ramo default?
Was this page helpful?