Override dei metodi in Java
Sovrascrivi i metodi della classe padre nelle sottoclassi Java con @Override e il dispatch dinamico.
L'override si verifica quando una sottoclasse dichiara un metodo con la stessa firma di uno ereditato, sostituendo la versione del padre. Combinato con il polimorfismo, è il meccanismo che consente di chiamare un metodo su un riferimento di tipo padre e ottenere l'esecuzione dell'implementazione corretta della sottoclasse.
Questo capitolo stabilisce le regole: cosa conta come override, cosa no, cosa il compilatore permette di modificare nella versione della sottoclasse e cosa garantisce realmente @Override.
Come appare un override
Un metodo sovrascrive quello ereditato quando si verificano tutte queste condizioni:
- Stesso nome.
- Stessa lista di parametri (tipi e ordine, dopo l'erasure dei generici).
- Tipo di ritorno uguale a quello del padre oppure un suo sottotipo (return covariante).
- Visibilità uguale a quella del padre o più ampia.
- Non dichiarato
final,staticoprivatenel padre.
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override
String speak() { return "meow"; }
}Questo è un override valido. Chiamare speak() su un Cat restituisce "meow"; chiamarlo attraverso un riferimento Animal che punta a un Cat restituisce anch'esso "meow" — il dispatch viene deciso dalla classe dell'oggetto reale.
@Override — usalo sempre
@Override è un'annotazione che dice al compilatore "intendo sovrascrivere un metodo ereditato." Se non sovrascrive effettivamente un metodo — nome errato, parametri errati, tipo di ritorno errato — il compilatore segnala un errore:
class Cat extends Animal {
@Override
String Speak() { return "meow"; } // ERROR — no Speak() in Animal
}Senza l'annotazione, questo compilerebbe silenziosamente come un metodo del tutto nuovo chiamato Speak, e ((Animal)cat).speak() continuerebbe a restituire "(noise)" del padre. L'annotazione non ha alcun costo e intercetta un'intera famiglia di bug silenziosi. Scrivila sempre sugli override.
Gli override che scriverai più spesso non riguardano le tue classi — si tratta di metodi ereditati da Object, la radice di ogni classe. Sovrascrivere toString(), equals(Object) e hashCode() è il modo in cui i tuoi oggetti si stampano in modo leggibile, si confrontano per valore e si comportano correttamente come chiavi in una HashMap o HashSet. @Override su questi intercetta il classico bug di scrivere equals(Cat other) (un overload) invece di equals(Object other) (il vero override).
Override vs overloading
| Aspetto | Override | Overloading |
|---|---|---|
| Dove | Tra una sottoclasse e una superclasse | Nella stessa classe |
| Firma | Deve corrispondere a quella del padre | Deve differire dalle altre |
| Dispatch | A runtime, basato sull'oggetto reale | A compile-time, basato sui tipi degli argomenti |
| Annotazione | @Override | nessuna |
Si confondono spesso. L'override è un metodo, diversi tipi di oggetti. L'overloading è più metodi, un tipo, scelto in base a ciò che si passa.
Tipi di ritorno covarianti
La sottoclasse può dichiarare un tipo di ritorno più specifico rispetto al padre:
class Animal {
Animal makeBaby() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat makeBaby() { return new Cat(); } // returns Cat, not Animal — fine
}Questa è la covarianza — il tipo di ritorno dell'override è un sottotipo di quello del padre. È sicuro perché qualsiasi chiamante che si aspetta un Animal da makeBaby() accetta volentieri un Cat. Il vantaggio è che i chiamanti che sanno di avere un Cat ricevono un Cat senza casting:
Cat kitten = new Cat().makeBaby(); // no cast neededFare il contrario (una sottoclasse che allarga il tipo di ritorno) non è consentito — romperebbe i chiamanti che si aspettavano il tipo più specifico.
La visibilità può solo allargarsi
L'override deve essere almeno altrettanto visibile quanto il metodo del padre:
class A { public void hi() { } }
class B extends A { protected void hi() { } } // ERROR — reduces visibility
class C extends A { public void hi() { } } // okRestringerla significherebbe che un chiamante con un riferimento A potrebbe chiamare hi() ma, dopo l'assegnazione A a = new B(), non potrebbe più — il che romperebbe la sostituibilità.
Le eccezioni possono solo ridursi
Se il metodo padre non dichiara eccezioni checked, l'override non può dichiararne nessuna. Se il padre lancia un'eccezione checked, l'override può lanciare la stessa o una sua sottoclasse più specifica — ma non una più ampia:
class A {
void run() throws IOException { ... }
}
class B extends A {
@Override void run() throws FileNotFoundException { ... } // ok — FileNotFoundException extends IOException
}
class C extends A {
@Override void run() throws Exception { ... } // ERROR — too broad
}Le eccezioni unchecked (sottoclassi di RuntimeException) sono senza restrizioni — puoi sempre lanciarle da un override.
Cosa non si può sovrascrivere
- Metodi
private. Non sono visibili alla sottoclasse, quindi un metodo con lo stesso nome nella sottoclasse è semplicemente un nuovo metodo. - Metodi
final. Contrassegnati specificamente per impedire gli override. - Metodi
static. Una sottoclasse può dichiarare un metodo statico con lo stesso nome e firma, ma questo si chiama method hiding, non override. Non c'è polimorfismo — la JVM risolve la chiamata in base al tipo a compile-time del riferimento:
class A { static String klass() { return "A"; } }
class B extends A { static String klass() { return "B"; } }
A a = new B();
System.out.println(a.klass()); // "A" — static, not polymorphicQuesto è uno dei pochi casi in cui la regola "il dispatch del metodo sceglie la versione dell'oggetto reale" non si applica.
Chiamare il padre dall'override
Un pattern comune è estendere anziché sostituire il comportamento del padre usando super.method(...):
class Logger {
void log(String s) { System.out.println(s); }
}
class TimestampedLogger extends Logger {
@Override
void log(String s) {
super.log("[" + System.currentTimeMillis() + "] " + s);
}
}L'override decora il comportamento del padre invece di duplicarlo. Trattato nel capitolo sulla parola chiave super.
Esempio pratico
Prossimi passi
Hai visto come le sottoclassi sostituiscono i metodi del padre. L'idea successiva — l'astrazione — è il rovescio della medaglia: dichiarare un metodo che non ha corpo predefinito e obbligare ogni sottoclasse concreta a fornirne uno. Continua con astrazione in Java.