1. Override dei metodi
L’override (overriding) è la possibilità, in una sottoclasse, di scrivere una propria versione di un metodo già presente nella classe padre. Grazie a questo funziona il vero polimorfismo durante l’esecuzione del programma (run-time).
In parole semplici:
Se avete una classe base con un metodo e volete che la sottoclasse esegua quel metodo a modo suo, dichiarate semplicemente un metodo con la stessa firma nella sottoclasse. Quando si chiama il metodo tramite un riferimento di tipo base, verrà eseguita la versione del metodo del tipo reale (effettivo) dell’oggetto.
Esempio dalla vita reale.
Immaginate di avere una squadra di animali e di chiedere a ciascuno «emettere un suono». Per tutti il comando è makeSound(), ma il cane abbaia, il gatto miagola e la mucca muggisce. Nel codice sembra la chiamata dello stesso metodo, ma il risultato è diverso — ecco la “magia” dell’override!
Sintassi dell’override
Esempio base
class Animal {
void makeSound() {
System.out.println("Un animale emette un suono");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miao!");
}
}
Qui nella classe Animal è definito il metodo makeSound(). Le sottoclassi Dog e Cat sovrascrivono questo metodo fornendo la propria implementazione.
Annotazione @Override
In Java è consigliabile (ed è una buona abitudine) contrassegnare i metodi sovrascritti con l’annotazione @Override:
@Override
void makeSound() { ... }
Non è obbligatoria per il funzionamento del codice, ma:
- Il compilatore verificherà che stiate davvero sovrascrivendo un metodo (e non che vi siate accidentalmente sbagliati nel nome o nei parametri).
- Migliora la leggibilità del codice — agli altri programmatori è subito chiaro che questo metodo sovrascrive quello del genitore.
Invocazione di un metodo sovrascritto
Animal myDog = new Dog();
myDog.makeSound(); // Stamperà: "Bau!"
Sebbene la variabile sia di tipo Animal, in realtà punta a un oggetto Dog e viene invocato il metodo della classe Dog. Questo è il binding dinamico (tardivo).
2. Differenza tra overriding e overloading
Overloading
- Nella stessa classe (o nella gerarchia, ma sempre “vicino”).
- I metodi hanno lo stesso nome, ma parametri diversi (tipo, quantità, ordine).
- La scelta del metodo avviene in fase di compilazione.
void print(int x) { ... }
void print(String s) { ... }
Overriding
- In classi diverse: il metodo è dichiarato nella superclasse ed è sovrascritto nella sottoclasse.
- I metodi hanno lo stesso nome e la stessa firma (parametri e tipo restituito).
- La scelta del metodo avviene durante l’esecuzione (run-time), in base al tipo effettivo dell’oggetto.
Tabella di confronto
| Overloading | Overriding | |
|---|---|---|
| Dove | Nella stessa classe | In superclasse e sottoclasse |
| Nome del metodo | Uguale | Uguale |
| Parametri | Diversi | Uguali |
| Tipo restituito | Può differire | Deve coincidere o essere un sottotipo |
| Quando viene scelto | Compilazione | Esecuzione (run-time) |
| Annotazione | Non richiesto | @Override (consigliato) |
3. Regole dell’override dei metodi
L’override è potente, ma comporta regole rigorose. Vediamole in ordine.
La firma del metodo deve coincidere
- Il nome del metodo, i tipi e l’ordine dei parametri devono essere identici al metodo nella superclasse.
- Il tipo restituito deve coincidere o essere covariante (cioè un sottotipo del tipo restituito dal metodo del genitore).
Esempio con tipo restituito covariante:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Cat extends Animal {
@Override
Cat reproduce() { return new Cat(); } // OK! Cat è un sottotipo di Animal
}
Modificatore di accesso
Il modificatore di accesso del metodo sovrascritto non può essere più restrittivo rispetto a quello del metodo nella superclasse. Se il metodo nel padre è public, allora nella sottoclasse deve rimanere public. Non è possibile renderlo meno accessibile (protected o private).
Esempio:
class Parent {
public void greet() { }
}
class Child extends Parent {
// void greet() { } // Errore! Il modificatore predefinito — package-private — è meno accessibile di public
@Override
public void greet() { } // OK
}
Eccezioni
- Il metodo sovrascritto non può lanciare nuove eccezioni checked che non siano dichiarate nel metodo base.
- Si possono lanciare meno eccezioni o le stesse.
Esempio:
class Parent {
void doWork() throws IOException { }
}
class Child extends Parent {
@Override
void doWork() throws FileNotFoundException { } // OK, FileNotFoundException è un sottotipo di IOException
// void doWork() throws SQLException { } // Errore! SQLException non è dichiarata nel padre
}
I metodi statici non vengono sovrascritti
I metodi statici possono essere nascosti (hidden), ma non sovrascritti. Se dichiarate un metodo statico con la stessa firma nella sottoclasse, non si tratta di override! È semplicemente occultamento del metodo, non polimorfismo.
class Animal {
static void info() { System.out.println("Animal"); }
}
class Dog extends Animal {
static void info() { System.out.println("Dog"); }
}
La chiamata Dog.info() stamperà "Dog", ma se la si invoca tramite una variabile di tipo Animal, verrà chiamato il metodo Animal.info(). Questo non è polimorfismo!
I metodi final non possono essere sovrascritti
Se un metodo nella superclasse è dichiarato come final, il tentativo di sovrascriverlo porterà a un errore di compilazione.
class Animal {
final void sleep() { }
}
class Dog extends Animal {
// @Override
// void sleep() { } // Errore! Non si può eseguire override di un metodo final
}
4. Esempi pratici
Vediamo in pratica come funziona l’override e in cosa si differenzia dall’overloading.
Esempio 1: la classe Shape e i suoi discendenti
class Shape {
void draw() {
System.out.println("Disegniamo una figura");
}
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Disegniamo un cerchio");
}
}
class Rectangle extends Shape {
@Override
void draw() {
System.out.println("Disegniamo un rettangolo");
}
}
Usiamo il polimorfismo:
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // Disegniamo un cerchio
s2.draw(); // Disegniamo un rettangolo
}
}
Sebbene le variabili siano dichiarate come Shape, viene invocato il metodo della classe a cui l’oggetto appartiene realmente.
Esempio 2: differenza rispetto all’overloading
class Printer {
void print(String s) {
System.out.println("Stringa: " + s);
}
void print(int n) {
System.out.println("Numero: " + n);
}
}
Qui entrambi i metodi si chiamano print, ma hanno parametri diversi — è overloading, non overriding.
5. Override e chiamata del metodo del genitore (super)
A volte, nel metodo sovrascritto, si vuole prima eseguire la logica del genitore e poi aggiungere la propria. Per questo si usa la parola chiave super.
class Animal {
void makeSound() {
System.out.println("L'animale emette un suono");
}
}
class Dog extends Animal {
@Override
void makeSound() {
super.makeSound(); // Chiamata al metodo del genitore
System.out.println("Bau!");
}
}
La chiamata new Dog().makeSound() stamperà:
L'animale emette un suono
Bau!
Come funziona il collegamento dinamico (late binding)
Quando chiamate un metodo tramite un riferimento al tipo base, Java durante l’esecuzione verifica a quale oggetto reale si riferisce quel riferimento e invoca proprio la versione del metodo definita nella classe di quell’oggetto.
Animal a = new Cat();
a.makeSound(); // Invoca Cat.makeSound(), non Animal.makeSound()
Questa è la base del polimorfismo in Java.
6. Come si collega alla tua applicazione
Nel nostro applicativo didattico (ad esempio, un sistema di gestione dei dipendenti) puoi creare una classe base Employee con il metodo work(), e le sottoclassi Manager e Developer possono implementare questo metodo a modo loro:
class Employee {
void work() {
System.out.println("Il dipendente lavora");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Il manager dirige");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Lo sviluppatore scrive codice");
}
}
Ora puoi memorizzare tutti i dipendenti in un unico array o elenco:
Employee[] employees = {new Manager(), new Developer(), new Developer()};
for (Employee e : employees) {
e.work(); // Per ciascuno — il proprio output!
}
7. Errori tipici nell’override dei metodi
Errore n. 1: refuso nel nome del metodo o nei parametri. Se sbagliate per errore il nome del metodo o i suoi parametri, non state sovrascrivendo il metodo, ma ne create uno nuovo. Di conseguenza, il polimorfismo non funziona. Ecco perché usate sempre l’annotazione @Override — il compilatore vi correggerà subito.
Errore n. 2: modificatore di accesso più restrittivo. Se nel genitore il metodo è public e nella classe figlia lo dichiarate come protected o senza modificatore, otterrete un errore di compilazione.
Errore n. 3: tentativo di sovrascrivere un metodo static o final. I metodi statici non si sovrascrivono e i metodi final non possono essere sovrascritti. Se ci provate, il compilatore vi fermerà.
Errore n. 4: cambiare il tipo restituito in uno incompatibile. Se il tipo restituito del metodo nella sottoclasse non coincide con quello nella superclasse (e non è un suo sottotipo), il compilatore non consentirà l’override.
Errore n. 5: aggiunta di nuove eccezioni checked. Un metodo sovrascritto non può lanciare nuove eccezioni checked che non sono presenti nella dichiarazione del metodo base. In caso contrario, il compilatore genererà un errore.
Errore n. 6: dimenticare super. Se nel metodo sovrascritto desiderate mantenere parte del comportamento del genitore, non dimenticate di chiamare esplicitamente super.methodName(). Java non lo farà da sola.
Ora sapete come funziona l’override dei metodi, in cosa si differenzia dall’overloading e come, grazie ad esso, si realizza il polimorfismo in Java. Nella prossima lezione vedremo come applicare il polimorfismo nella pratica — con collezioni, array e compiti reali!
GO TO FULL VERSION