1. Ridefinizione del metodo
Nella vita reale capita spesso che un «discendente» si comporti in modo particolare. Per esempio, tutti gli animali sanno emettere suoni, ma il gatto fa «miao», il cane «bau», e il programmatore — «oh, di nuovo un bug!». In programmazione ciò si realizza tramite l'override del metodo (override).
L'override di un metodo è quando una sottoclasse fornisce la propria implementazione di un metodo già dichiarato nella classe padre. In altre parole, «sostituisce» il comportamento standard con uno più specifico.
Analogia. Se immaginiamo la classe padre come la ricetta originale del borsch, l'override del metodo è quando la nonna aggiunge il suo ingrediente segreto. Il borsch rimane borsch, ma il sapore cambia per ognuno.
Per ridefinire un metodo, occorre dichiarare nella classe figlia un metodo con esattamente la stessa firma (nome, parametri, tipo restituito) di quello del genitore.
Esempio: gli animali e i loro suoni
class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
// Override del metodo makeSound()
void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
// Override del metodo makeSound()
void makeSound() {
System.out.println("Meow!");
}
}
Ora, se create un oggetto Dog e chiamate makeSound(), sentirete "Woof!" invece di "Some generic animal sound".
Dimostrazione nel codice
public class Main {
public static void main(String[] args) {
Animal generic = new Animal();
Dog dog = new Dog();
Cat cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
}
}
Importante: se la sottoclasse non ha un metodo con la stessa firma, verrà utilizzato il metodo del genitore.
2. Annotazione @Override: a cosa serve e come usarla
In Java è prassi contrassegnare i metodi ridefiniti con la speciale annotazione @Override. Non è un semplice ornamento, ma uno strumento utile:
- Il compilatore verifica che stiate davvero ridefinendo un metodo del genitore. Se sbagliate nome, tipo dei parametri o tipo restituito, il compilatore segnalerà un errore.
- Migliora la leggibilità del codice. Un altro programmatore vede subito: «Oh, questo metodo ridefinisce quello della classe padre».
Esempio con @Override
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof!");
}
}
Se per errore scrivete void makeSond() (refuso!) in un metodo contrassegnato con @Override, il compilatore si lamenterà: "Method does not override or implement a method from a supertype".
Standard moderni. Usare @Override è buona pratica e standard di settore. Anche se il compilatore non lo richiede, inserite sempre questa annotazione: semplificherà la vita a voi e ai colleghi.
3. Come funziona la chiamata di un metodo ridefinito
Quando chiamate un metodo su un oggetto di una sottoclasse, verrà usata l'implementazione della sottoclasse anche se la variabile è dichiarata come tipo del genitore.
Esempio: polimorfismo in azione
Animal animal = new Dog();
animal.makeSound(); // "Woof!", non "Some generic animal sound"
Qui la variabile è di tipo Animal, ma in realtà contiene un oggetto Dog. Java «capisce» che deve chiamare il metodo ridefinito di Dog. Questo è il polimorfismo (ne parleremo in dettaglio nelle prossime lezioni).
4. Limitazioni e regole dell'override
Firma del metodo
- Nome, tipo e ordine dei parametri devono coincidere con il metodo del genitore.
- Il tipo restituito deve coincidere oppure essere covariante (un sottotipo del tipo restituito dal genitore). Per esempio, se il genitore restituisce Animal e il figlio Dog, è consentito.
Modificatori di accesso
- Non si può rendere l'accesso più restrittivo rispetto al genitore.
- Se il metodo del genitore è public, anche quello ridefinito deve essere public.
- Se il genitore è protected, quello ridefinito può essere protected o public.
Se provate a fare il contrario, il compilatore dirà: "Cannot reduce the visibility of the inherited method".
Eccezioni
- Un metodo ridefinito non può lanciare una nuova eccezione checked non presente nella dichiarazione del genitore.
- Si possono lanciare meno eccezioni rispetto al genitore, o loro sottotipi.
static, final, private
- Non è possibile ridefinire metodi dichiarati come static o final, nonché quelli privati (private).
- static equivale a hiding (occultamento), non a override.
- final non può essere ridefinito: Java protegge tali metodi.
- private non è visibile nella sottoclasse, quindi non può essere ridefinito (si può solo dichiarare un nuovo metodo con lo stesso nome).
Costruttori
I costruttori non vengono ereditati né ridefiniti. Ogni classe ha i propri costruttori.
5. Sviluppiamo l'app didattica «Zoo»
È ora di mettere in pratica la teoria! Continuiamo a sviluppare la nostra applicazione «Zoo».
Passo 1. Classe base Animal
public class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
public void sleep() {
System.out.println("Zzz...");
}
}
Passo 2. Sottoclassi Dog e Cat
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
// Metodo aggiuntivo solo per Dog
public void fetch() {
System.out.println("Dog brings the stick!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
// Metodo aggiuntivo solo per Cat
public void scratch() {
System.out.println("Cat scratches the sofa!");
}
}
Passo 3. Usiamo l'override
public class ZooTest {
public static void main(String[] args) {
Animal generic = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
generic.makeSound(); // Some generic animal sound
dog.makeSound(); // Woof!
cat.makeSound(); // Meow!
// dog.fetch(); // Errore! Una variabile di tipo Animal non conosce fetch()
// cat.scratch(); // Analogamente
// Ma se specifichiamo esplicitamente il tipo:
if (dog instanceof Dog) {
((Dog) dog).fetch(); // Dog brings the stick!
}
if (cat instanceof Cat) {
((Cat) cat).scratch(); // Cat scratches the sofa!
}
}
}
Commento:
Il metodo makeSound() funziona in modo polimorfico — viene chiamata la versione della classe reale dell'oggetto. I metodi specifici (fetch, scratch) invece sono disponibili solo tramite cast esplicito — è importante per capire come funzionano ereditarietà e override.
6. Esempio con tipo di ritorno (covarianza)
A volte si desidera che il metodo ridefinito restituisca un tipo più «stretto». Per esempio:
class Animal {
Animal getFriend() {
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog getFriend() { // Tipo restituito — Dog, sottotipo di Animal
return new Dog();
}
}
Questo si chiama covarianza del tipo restituito ed è consentito in Java (a partire da Java 5).
7. Cosa succede se non si usa @Override?
Se sbagliate per caso il nome del metodo o i parametri, Java non si lamenterà se non c'è l'annotazione @Override. Di conseguenza non farete un override, ma creerete un nuovo metodo, e il comportamento atteso non cambierà.
Esempio di errore
class Dog extends Animal {
// Refuso: makeSoud invece di makeSound
void makeSoud() {
System.out.println("Woof!");
}
}
public class Main {
public static void main(String[] args) {
Animal dog = new Dog();
dog.makeSound(); // Stamperà "Some generic animal sound"
}
}
Se ci fosse stato @Override, il compilatore avrebbe segnalato l'errore: "Method does not override or implement a method from a supertype".
8. Errori tipici nell'override dei metodi
Errore n. 1: assenza dell'annotazione @Override.
Senza di essa è facile sbagliare nel nome del metodo o nei parametri. Di conseguenza il metodo non verrà ridefinito e il programma si comporterà diversamente da quanto previsto.
Errore n. 2: tentativo di restringere il modificatore di accesso.
Se il metodo del genitore è public e voi scrivete protected o private, otterrete un errore di compilazione.
Errore n. 3: la firma non coincide.
Se i parametri differiscono anche solo per tipo, non è override ma overloading (overloading).
Errore n. 4: tentativo di ridefinire un metodo final o static.
Java non lo permette: final protegge dall'override, e i metodi static non vengono affatto ridefiniti (solo nascosti).
Errore n. 5: cambiare il tipo di ritorno in uno incompatibile.
Si può restituire solo un sottotipo del tipo restituito dal genitore (covarianza), non un tipo completamente diverso.
GO TO FULL VERSION