Menu

Ereditarietà in Java: extends, super e override

Come una sottoclasse Java eredita campi e metodi con extends, chiama la classe madre tramite super e ridefinisce il comportamento, con le trappole più comuni.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

L'ereditarietà è riuso di tipo "è un"

L'ereditarietà permette a una nuova classe di basarsi su una esistente. La nuova classe, la sottoclasse, riceve automaticamente i campi e i metodi della superclasse, poi aggiunge o modifica ciò che le serve. La usi quando un tipo è una variante più specifica di un altro: un Dog è un Animal, un SavingsAccount è un BankAccount.

La scrivi con la parola chiave extends. Tutto ciò che è public o protected nella classe madre è disponibile nella figlia senza doverlo riscrivere.

Dog non dichiara mai name né eat(), eppure li ha entrambi. È proprio questo il punto: il comportamento condiviso vive in un solo posto.

super: raggiungere la classe madre

Il costruttore di una sottoclasse deve assicurarsi che la classe madre sia inizializzata per prima. Lo fai con super(...), che chiama il costruttore della classe madre e deve essere la prima istruzione del costruttore della sottoclasse. Se lo ometti, Java inserisce in silenzio una chiamata al costruttore senza argomenti della classe madre, e se la classe madre non ne ha uno, il codice non compila.

L'output mostra che il costruttore della classe madre viene eseguito prima del corpo di quello della figlia: la costruzione scende dalla cima della gerarchia verso il basso.

Override dei metodi

Una sottoclasse può sostituire un metodo ereditato ridefinendolo con la stessa firma. Questo si chiama override, e dovresti sempre marcarlo con @Override. L'annotazione non è obbligatoria, ma fa verificare al compilatore che tu abbia davvero corrisposto a un metodo della classe madre: intercetta errori di battitura come tostring() al posto di toString(), che altrimenti creerebbero in silenzio un metodo completamente nuovo.

Anche se l'array è di tipo Animal, ogni elemento esegue il proprio speak(). Java sceglie il metodo in base all'oggetto reale a runtime, non al tipo dichiarato della variabile: è la base del polimorfismo.

Chiamare la versione della classe madre con super.method()

Fare l'override non significa buttare via il lavoro della classe madre. Usa super.method() per eseguire la versione ereditata e poi aggiungere qualcosa:

Senza super., chiamare log dentro TimestampLogger.log richiamerebbe se stesso all'infinito. super. significa esplicitamente "la versione della classe madre".

Campi ereditati e accesso

Una sottoclasse vede i membri public e protected della classe madre ma non quelli private. I campi private esistono comunque nell'oggetto, e i metodi della classe madre possono usarli, ma la sottoclasse non può riferirsi a loro direttamente. Usa protected, uno dei modificatori di accesso di Java, quando vuoi dare accesso alle sottoclassi tenendo il membro nascosto al codice non correlato.

class Base {
    private int secret;      // invisibile alle sottoclassi
    protected int shared;    // visibile alle sottoclassi
}

class Derived extends Base {
    void demo() {
        shared = 5;          // OK
        // secret = 5;       // errore di compilazione: private in Base
    }
}

È anche per questo che il costruttore di una sottoclasse spesso deve chiamare super(...): è l'unico modo per inizializzare lo stato privato della classe madre.

Bloccare l'ereditarietà con final

A volte una classe non dovrebbe essere estesa affatto: String è final proprio per questo motivo. Marcare una classe come final vieta di crearne sottoclassi; marcare un metodo come final ne vieta l'override pur permettendo di estendere la classe.

final class Constants { }            // non può avere sottoclassi

class Config {
    final void load() { }            // le sottoclassi possono estendere Config
                                     // ma non possono fare l'override di load()
}

Usa final quando il comportamento di una classe deve essere garantito e immutabile in tutto il programma: è un segnale deliberato di "non estendere", non l'impostazione predefinita.

Una trappola comune: preferisci la composizione se non è "è un"

L'ereditarietà è allettante perché riutilizza codice, ma lega strettamente la figlia alla madre. Se la relazione non è un vero "è un", per esempio una Car che ha bisogno di un Engine, non scrivere Car extends Engine. Un'auto ha un motore, non è un motore. Rappresentalo invece con un campo (composizione):

class Car {
    private Engine engine = new Engine();   // Car HA-UN Engine

    void start() { engine.ignite(); }
}

Usa l'ereditarietà solo quando la sottoclasse è davvero una forma specializzata della superclasse e vuoi sia ereditarne sia sostituirne il comportamento.

Prossimo passo: le interfacce

L'ereditarietà con extends ti dà una sola classe madre e un'implementazione condivisa. Ma una classe può estendere una sola classe: come fai allora a dare una capacità comune a classi non correlate? È a questo che servono le interfacce: un contratto che molte classi possono implementare, ed è l'argomento della prossima pagina.

Domande frequenti

Cos'è l'ereditarietà in Java?

L'ereditarietà permette a una classe (la sottoclasse) di riutilizzare i campi e i metodi di un'altra classe (la superclasse) tramite la parola chiave extends. La sottoclasse riceve automaticamente i membri public e protected della classe madre e può aggiungerne di nuovi o sostituire il comportamento ereditato con l'override. Rappresenta una relazione "è un": un Dog è un Animal.

Cosa fa la parola chiave super in Java?

super si riferisce alla classe madre. super(...) in un costruttore chiama il costruttore della classe madre (e deve essere la prima istruzione), mentre super.method() chiama la versione della classe madre di un metodo di cui hai fatto l'override. Permette a una sottoclasse di costruire sulla logica della classe madre invece di sostituirla del tutto.

Qual è la differenza tra override e overloading in Java?

L'override ridefinisce in una sottoclasse un metodo ereditato usando la stessa firma, cambiandone il comportamento: contrassegnalo con @Override. L'overloading definisce nella stessa classe più metodi con lo stesso nome ma liste di parametri diverse. L'override riguarda l'ereditarietà e la scelta del metodo a runtime; l'overloading è solo una coppia di metodi che condividono lo stesso nome.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA