Classi Sealed in Java

Edoardo Midali
Edoardo Midali

Le classi sealed rappresentano una delle innovazioni più significative introdotte in Java 17, offrendo un controllo granulare sull’ereditarietà e permettendo la creazione di gerarchie di classi ristrette e ben definite. Questo meccanismo colma il divario tra classi completamente aperte all’estensione e classi final che impediscono qualsiasi estensione.

Concetto e Motivazione

Le classi sealed introducono un terzo modello di ereditarietà che si posiziona tra la completa apertura e la chiusura totale. Permettono al designer della classe di specificare esattamente quali classi possono estenderla, creando gerarchie controllate che migliorano la sicurezza del tipo e facilitano l’analisi statica del codice.

Problemi che Risolvono

Controllo dell’Estensione: Prima delle classi sealed, una classe era o completamente aperta all’estensione (permettendo a qualsiasi classe di estenderla) o completamente chiusa (final). Non esisteva una via di mezzo per permettere solo a specifiche classi di estendere una superclasse.

Pattern Matching Sicuro: Le classi sealed abilitano pattern matching esaustivo, dove il compilatore può verificare che tutti i possibili sottotipi siano gestiti in un’istruzione switch o in altri costrutti di controllo.

Design Intent Esplicito: Rendono esplicita l’intenzione del designer riguardo a quali classi sono parte di una gerarchia controllata, migliorando la documentazione vivente del codice.

// Prima di sealed classes - problematico
public abstract class Forma {
    public abstract double calcolaArea();
}

// Chiunque può estendere Forma, rendendo impossibile
// un pattern matching esaustivo sicuro
class FormaStrana extends Forma {
    public double calcolaArea() { return -1; } // Implementazione problematica
}

Sintassi e Dichiarazione

Una classe sealed utilizza la parola chiave sealed e deve specificare esplicitamente quali classi possono estenderla attraverso la clausola permits. Le classi che estendono una sealed class devono essere dichiarate come final, sealed, o non-sealed.

// Dichiarazione di una classe sealed
public sealed class Veicolo
    permits Automobile, Motocicletta, Autobus {

    protected String marca;
    protected String modello;

    public Veicolo(String marca, String modello) {
        this.marca = marca;
        this.modello = modello;
    }

    public abstract double calcolaConsumo(double distanza);

    public String getInfo() {
        return marca + " " + modello;
    }
}

// Sottoclasse final - termina la catena di ereditarietà
public final class Automobile extends Veicolo {
    private int numeroPorte;
    private double consumoPerKm;

    public Automobile(String marca, String modello, int numeroPorte, double consumo) {
        super(marca, modello);
        this.numeroPorte = numeroPorte;
        this.consumoPerKm = consumo;
    }

    @Override
    public double calcolaConsumo(double distanza) {
        return distanza * consumoPerKm;
    }
}

// Sottoclasse sealed - può essere ulteriormente estesa
public sealed class Motocicletta extends Veicolo
    permits MotocrossMotor, TouringMotor {

    protected int cilindrata;

    public Motocicletta(String marca, String modello, int cilindrata) {
        super(marca, modello);
        this.cilindrata = cilindrata;
    }

    @Override
    public double calcolaConsumo(double distanza) {
        return distanza * (cilindrata / 1000.0) * 0.05;
    }
}

// Sottoclasse non-sealed - riapre la gerarchia
public non-sealed class Autobus extends Veicolo {
    private int capacitaPasseggeri;

    public Autobus(String marca, String modello, int capacita) {
        super(marca, modello);
        this.capacitaPasseggeri = capacita;
    }

    @Override
    public double calcolaConsumo(double distanza) {
        return distanza * 0.3 * (capacitaPasseggeri / 50.0);
    }
}

// Ora qualsiasi classe può estendere Autobus
class AutobusUrbano extends Autobus {
    public AutobusUrbano(String marca, String modello) {
        super(marca, modello, 40);
    }
}

Pattern Matching Esaustivo

Una delle applicazioni più potenti delle classi sealed è l’abilitazione del pattern matching esaustivo, dove il compilatore può verificare che tutti i possibili sottotipi siano gestiti.

public class CalcolatoreVeicoli {

    public static String classificaVeicolo(Veicolo veicolo) {
        // Pattern matching esaustivo - il compilatore verifica
        // che tutti i sottotipi sealed siano gestiti
        return switch (veicolo) {
            case Automobile auto ->
                "Auto con " + auto.getNumeroPorte() + " porte";
            case Motocicletta moto ->
                "Moto con cilindrata " + moto.getCilindrata() + "cc";
            case Autobus autobus ->
                "Autobus con capacità " + autobus.getCapacitaPasseggeri() + " passeggeri";
            // Non serve default case - il compilatore sa che questi sono tutti i casi
        };
    }

    public static double calcolaCostoAssicurazione(Veicolo veicolo) {
        return switch (veicolo) {
            case Automobile auto -> {
                double baseRate = 500.0;
                yield auto.getNumeroPorte() > 2 ? baseRate * 1.2 : baseRate;
            }
            case Motocicletta moto -> {
                double baseRate = 300.0;
                yield moto.getCilindrata() > 600 ? baseRate * 1.5 : baseRate;
            }
            case Autobus autobus -> 1500.0; // Tariffa fissa per autobus
        };
    }

    // Metodo che dimostra type safety
    public static void analizzaFlotta(List<Veicolo> flotta) {
        for (Veicolo veicolo : flotta) {
            String tipo = switch (veicolo) {
                case Automobile auto -> "Veicolo privato";
                case Motocicletta moto -> "Veicolo a due ruote";
                case Autobus autobus -> "Trasporto pubblico";
            };

            System.out.println(veicolo.getInfo() + " - " + tipo);
        }
    }
}

Sealed Interfaces

Le interfaces possono essere sealed esattamente come le classi, offrendo controllo granulare su quali classi o interfaces possono implementarle.

public sealed interface Processore
    permits ProcessoreBase, ProcessoreAvanzato, ProcessoreSpecializzato {

    void elabora(String input);
    String getTipo();
}

public final class ProcessoreBase implements Processore {
    @Override
    public void elabora(String input) {
        System.out.println("Elaborazione base: " + input.toUpperCase());
    }

    @Override
    public String getTipo() {
        return "BASE";
    }
}

public sealed class ProcessoreAvanzato implements Processore
    permits ProcessoreML, ProcessoreIA {

    protected String algoritmo;

    public ProcessoreAvanzato(String algoritmo) {
        this.algoritmo = algoritmo;
    }

    @Override
    public void elabora(String input) {
        System.out.println("Elaborazione avanzata con " + algoritmo + ": " + input);
    }

    @Override
    public String getTipo() {
        return "AVANZATO";
    }
}

public non-sealed interface ProcessoreSpecializzato extends Processore {
    void configuraParametri(Map<String, Object> parametri);
}

// Implementazione che riapre la gerarchia
public class ProcessoreCustom implements ProcessoreSpecializzato {
    private Map<String, Object> config;

    @Override
    public void elabora(String input) {
        System.out.println("Elaborazione personalizzata: " + input);
    }

    @Override
    public String getTipo() {
        return "CUSTOM";
    }

    @Override
    public void configuraParametri(Map<String, Object> parametri) {
        this.config = new HashMap<>(parametri);
    }
}

Gerarchie Complesse e Nested Sealing

Le classi sealed possono creare gerarchie complesse con multiple restrizioni annidate, permettendo controllo granulare a diversi livelli.

public sealed class Documento
    permits DocumentoTesto, DocumentoMultimediale, DocumentoStrutturato {

    protected String titolo;
    protected LocalDateTime dataCreazione;
    protected String autore;

    public Documento(String titolo, String autore) {
        this.titolo = titolo;
        this.autore = autore;
        this.dataCreazione = LocalDateTime.now();
    }

    public abstract long calcolaDimensione();
    public abstract String getFormato();
}

public sealed class DocumentoTesto extends Documento
    permits DocumentoWord, DocumentoPDF, DocumentoPlainText {

    protected String contenutoTestuale;

    public DocumentoTesto(String titolo, String autore, String contenuto) {
        super(titolo, autore);
        this.contenutoTestuale = contenuto;
    }

    @Override
    public long calcolaDimensione() {
        return contenutoTestuale.length() * 2; // Stima approssimativa
    }
}

public final class DocumentoWord extends DocumentoTesto {
    private List<String> stili;
    private boolean haTabelle;

    public DocumentoWord(String titolo, String autore, String contenuto) {
        super(titolo, autore, contenuto);
        this.stili = new ArrayList<>();
        this.haTabelle = false;
    }

    @Override
    public String getFormato() {
        return "Microsoft Word (.docx)";
    }

    @Override
    public long calcolaDimensione() {
        long dimensioneBase = super.calcolaDimensione();
        long overheadStili = stili.size() * 100;
        long overheadTabelle = haTabelle ? 1000 : 0;
        return dimensioneBase + overheadStili + overheadTabelle;
    }
}

public final class DocumentoPDF extends DocumentoTesto {
    private int numeroPagine;
    private boolean hasImmagini;

    public DocumentoPDF(String titolo, String autore, String contenuto, int pagine) {
        super(titolo, autore, contenuto);
        this.numeroPagine = pagine;
        this.hasImmagini = false;
    }

    @Override
    public String getFormato() {
        return "Portable Document Format (.pdf)";
    }

    @Override
    public long calcolaDimensione() {
        long dimensioneBase = super.calcolaDimensione();
        long overheadPagine = numeroPagine * 500;
        long overheadImmagini = hasImmagini ? 50000 : 0;
        return dimensioneBase + overheadPagine + overheadImmagini;
    }
}

public final class DocumentoPlainText extends DocumentoTesto {
    private String encoding;

    public DocumentoPlainText(String titolo, String autore, String contenuto) {
        super(titolo, autore, contenuto);
        this.encoding = "UTF-8";
    }

    @Override
    public String getFormato() {
        return "Plain Text (.txt)";
    }
}

Integration con Record Classes

Le sealed classes si integrano perfettamente con le record classes, creando gerarchie di dati immutabili e type-safe.

public sealed interface Risultato<T>
    permits Successo, Errore {
}

public record Successo<T>(T valore) implements Risultato<T> {
    public Successo {
        Objects.requireNonNull(valore, "Il valore non può essere null");
    }
}

public record Errore<T>(String messaggio, Throwable causa) implements Risultato<T> {
    public Errore {
        Objects.requireNonNull(messaggio, "Il messaggio di errore non può essere null");
    }

    public Errore(String messaggio) {
        this(messaggio, null);
    }
}

// Utilizzo con pattern matching
public class GestoreRisultati {

    public static <T> void processaRisultato(Risultato<T> risultato) {
        switch (risultato) {
            case Successo<T>(var valore) -> {
                System.out.println("Operazione riuscita con valore: " + valore);
                // Elabora il valore di successo
            }
            case Errore<T>(var messaggio, var causa) -> {
                System.err.println("Errore: " + messaggio);
                if (causa != null) {
                    causa.printStackTrace();
                }
            }
        }
    }

    public static <T> T estraiValore(Risultato<T> risultato, T valoreDefault) {
        return switch (risultato) {
            case Successo<T>(var valore) -> valore;
            case Errore<T>(var messaggio, var causa) -> {
                System.err.println("Usando valore default a causa di: " + messaggio);
                yield valoreDefault;
            }
        };
    }
}

Design Patterns con Sealed Classes

Visitor Pattern Semplificato

Le sealed classes eliminano la necessità del tradizionale Visitor pattern in molti casi, sostituendolo con pattern matching diretto.

public sealed interface Espressione
    permits Numero, Addizione, Moltiplicazione, Variabile {
}

public record Numero(double valore) implements Espressione {}
public record Addizione(Espressione sinistra, Espressione destra) implements Espressione {}
public record Moltiplicazione(Espressione sinistra, Espressione destra) implements Espressione {}
public record Variabile(String nome) implements Espressione {}

public class CalcolatoreEspressioni {

    public static double valuta(Espressione expr, Map<String, Double> variabili) {
        return switch (expr) {
            case Numero(var valore) -> valore;
            case Addizione(var sin, var des) ->
                valuta(sin, variabili) + valuta(des, variabili);
            case Moltiplicazione(var sin, var des) ->
                valuta(sin, variabili) * valuta(des, variabili);
            case Variabile(var nome) ->
                variabili.getOrDefault(nome, 0.0);
        };
    }

    public static String convertiInStringa(Espressione expr) {
        return switch (expr) {
            case Numero(var valore) -> String.valueOf(valore);
            case Addizione(var sin, var des) ->
                "(" + convertiInStringa(sin) + " + " + convertiInStringa(des) + ")";
            case Moltiplicazione(var sin, var des) ->
                "(" + convertiInStringa(sin) + " * " + convertiInStringa(des) + ")";
            case Variabile(var nome) -> nome;
        };
    }
}

Considerazioni su Performance e Memory

Le sealed classes non introducono overhead di performance significativo rispetto alle classi normali. Il controllo delle permits viene effettuato a compile-time, non a runtime.

// Benchmark-friendly design con sealed classes
public sealed interface Operazione
    permits OperazioneVeloce, OperazioneMedia, OperazioneLenta {

    long esegui(long input);
}

public record OperazioneVeloce() implements Operazione {
    @Override
    public long esegui(long input) {
        return input * 2; // Operazione O(1)
    }
}

public record OperazioneMedia() implements Operazione {
    @Override
    public long esegui(long input) {
        long risultato = 0;
        for (int i = 0; i < 1000; i++) {
            risultato += input;
        }
        return risultato;
    }
}

public record OperazioneLenta() implements Operazione {
    @Override
    public long esegui(long input) {
        return LongStream.range(0, input)
                        .reduce(0, Long::sum);
    }
}

Best Practices

Progetta per l’Estensibilità Controllata: Usa sealed classes quando vuoi permettere estensioni specifiche ma mantenere controllo sulla gerarchia.

Combina con Pattern Matching: Sfrutta il pattern matching esaustivo per creare codice più sicuro e leggibile.

Documenta le Decisioni di Design: Spiega perché hai scelto di rendere sealed una classe e quali sono le sottoclassi permesse.

Usa Record per Dati Immutabili: Combina sealed interfaces con record classes per creare gerarchie di dati type-safe.

Considera le Performance: Anche se l’overhead è minimo, testa le performance quando usi pattern matching intensivo.

Mantieni Gerarchie Semplici: Evita nesting eccessivo di sealed classes che può complicare la comprensione del codice.

Conclusione

Le sealed classes rappresentano un’evoluzione significativa del sistema di tipi Java, offrendo un controllo preciso sull’ereditarietà e abilitando pattern di programmazione più sicuri ed espressivi. Quando utilizzate appropriatamente, migliorano la type safety, facilitano il pattern matching esaustivo e rendono più esplicite le intenzioni di design.

La loro adozione è particolarmente vantaggiosa in domini dove è importante mantenere un controllo stretto sulle gerarchie di tipi, come nella modellazione di API pubbliche, nella creazione di DSL (Domain Specific Languages), e nell’implementazione di macchine a stati finiti.