1. Errori nell'ereditarietà
L'ereditarietà è uno dei fondamenti della OOP, ma anche uno degli argomenti in cui i principianti inciampano più spesso. Vediamo gli errori classici e impariamo a evitarli.
Mancata chiamata del costruttore della classe base (super(...))
Quando si crea una sottoclasse, è importante ricordare: la classe base può richiedere una certa inizializzazione tramite costruttore. Se nella classe base non c'è un costruttore di default (senza parametri), allora nel costruttore della sottoclasse è obbligatorio chiamare esplicitamente il costruttore della classe base con super(...).
Esempio di errore:
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
// Errore! Animal non ha un costruttore predefinito
public Dog() {
// super(); // il compilatore inserisce automaticamente super(), ma un tale costruttore non esiste!
}
}
Come correggere:
class Dog extends Animal {
public Dog(String name) {
super(name); // Tutto ok!
}
}
Commento:
Se nella classe base esiste solo un costruttore con parametri, il compilatore non aggiungerà automaticamente un costruttore senza parametri. È una causa frequente di errori di compilazione.
Tentativo di estendere una classe final o di sovrascrivere un metodo final
In Java è possibile dichiarare una classe o un metodo come final. Ciò significa:
- La classe non può essere estesa.
- Il metodo non può essere sovrascritto (override) nelle sottoclassi.
Esempio di errore:
final class Cat {}
// Errore di compilazione!
class Tiger extends Cat {
// ...
}
class Animal {
public final void sleep() {
System.out.println("Zzz...");
}
}
class Dog extends Animal {
// Errore di compilazione!
@Override
public void sleep() {
System.out.println("Dog is sleeping...");
}
}
Commento:
Se vedete l'errore "cannot inherit from final", "cannot override final method" — controllate i modificatori!
Violazione del principio di sostituzione di Liskov (Liskov Substitution Principle)
Sembra complicato, ma in pratica significa: un oggetto della sottoclasse deve comportarsi come un oggetto della classe base, senza rompere la logica del programma. Errore comune — sovrascrivere i metodi in modo che la nuova classe si comporti in maniera inattesa rispetto alla base.
Esempio:
class Bird {
public void fly() {
System.out.println("Sto volando!");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("I pinguini non volano!");
}
}
Qual è il problema?
Il codice che lavora con Bird si aspetta che qualsiasi uccello sappia volare. Ma se gli passate Penguin, il programma può andare in errore.
Meglio così:
In questi casi conviene rivedere l'ereditarietà o usare interfacce/composizione.
2. Errori con il sovraccarico dei metodi (overloading)
Il sovraccarico è quando in una stessa classe esistono più metodi con lo stesso nome ma parametri diversi. Sembra semplice, ma anche qui ci sono trappole.
Sovraccarico al posto di override (errore nella firma)
Spesso i principianti vogliono sovrascrivere (override) un metodo della classe base, ma cambiano accidentalmente i suoi parametri. Alla fine si ottiene un sovraccarico, non un override — e il polimorfismo non funziona!
Esempio di errore:
class Animal {
public void makeSound() {
System.out.println("Some sound");
}
}
class Dog extends Animal {
// Volevamo fare override, ma abbiamo fatto un sovraccarico!
public void makeSound(String extra) {
System.out.println("Bark! " + extra);
}
}
Problema:
La chiamata dog.makeSound() invocherà il metodo del genitore, non il vostro.
La chiamata dog.makeSound("loudly") invocherà quello sovraccaricato, ma il polimorfismo non funziona!
Best practice:
Usate l'annotazione @Override — se sbagliate la firma, il compilatore ve lo segnalerà subito.
@Override
public void makeSound() { /* ... */ }
Comportamenti non ovvi nel sovraccarico (conversione automatica dei tipi, ambiguità di chiamata)
A volte Java può "scegliere" un metodo diverso da quello atteso se i parametri corrispondono a più varianti sovraccaricate.
public class OverloadDemo {
public void print(int x) {
System.out.println("int: " + x);
}
public void print(double x) {
System.out.println("double: " + x);
}
}
OverloadDemo demo = new OverloadDemo();
demo.print(5); // int: 5
demo.print(5.0); // double: 5.0
demo.print(5L); // long -> double: double: 5.0
Problema:
Se si chiama demo.print(5L), Java sceglierà print(double x) (poiché long si converte meglio in double che in int).
Se esistono metodi con parametri di tipo Object, Integer, int, una chiamata con null può causare un errore di compilazione: "reference to print is ambiguous".
Utilizzare lo stesso nome di metodo con tipi di ritorno diversi (errore di compilazione)
In Java non è possibile dichiarare due metodi con lo stesso nome e la stessa lista di parametri che differiscono solo per il tipo di ritorno!
public class Demo {
// Errore di compilazione!
public int foo() { return 1; }
public String foo() { return "hello"; }
}
Spiegazione:
La firma del metodo per il sovraccarico è nome + parametri. Il tipo di ritorno non viene considerato. Il compilatore non potrà capire quale metodo volete chiamare.
3. Best practice
Per evitare problemi con ereditarietà e sovraccarico, attenetevi a queste raccomandazioni:
Usate sempre l'annotazione @Override per i metodi sovrascritti
Non solo migliora la leggibilità, ma vi protegge dagli errori nella firma. Se cambiate per sbaglio i parametri o il nome del metodo, il compilatore ve lo dirà subito.
@Override
public void makeSound() {
System.out.println("Bark!");
}
Distinguete chiaramente sovraccarico e override
- Override: cambiate il comportamento del metodo del genitore — la firma deve coincidere.
- Overload: aggiungete un nuovo metodo con lo stesso nome ma parametri diversi.
Tabella riassuntiva:
| Sovraccarico (overloading) | Override (overriding) | |
|---|---|---|
| Dove | Nella stessa classe/gerarchia | Nella sottoclasse |
| Nome del metodo | Uguale | Uguale |
| Parametri | Diversi | Coincidono |
| Tipo di ritorno | Può differire | Deve coincidere/essere compatibile |
| Annotazione | Non richiesta | @Override consigliata |
Non abusate del sovraccarico
Se un metodo ha troppe varianti sovraccaricate, il codice diventa illeggibile e confuso. Meglio usare oggetti-parametro o il pattern Builder, se le varianti sono troppe.
4. Esempio: sistema di gestione degli animali domestici
Supponiamo stiate creando un semplice sistema di gestione per animali domestici. Avete una classe base Pet e le sottoclassi Cat e Dog.
public class Pet {
private String name;
public Pet(String name) {
this.name = name;
}
public void speak() {
System.out.println(name + " emette un suono indefinito.");
}
}
public class Cat extends Pet {
public Cat(String name) {
super(name);
}
@Override
public void speak() {
System.out.println(getName() + " dice: Miao!");
}
// Errore: in Pet non esiste il metodo getName()!
}
Errore tipico:
Nel tentativo di sovrascrivere il metodo, ci riferiamo a un metodo che nella classe base non esiste. Meglio aggiungere un getter:
public class Pet {
private String name;
public Pet(String name) { this.name = name; }
public String getName() { return name; }
public void speak() { System.out.println(name + " emette un suono indefinito."); }
}
Ora tutto funziona correttamente e possiamo usare il polimorfismo:
Pet myPet = new Cat("Barsik");
myPet.speak(); // Barsik dice: Miao!
5. Errori tipici con ereditarietà e sovraccarico
Errore n. 1: vi siete dimenticati di chiamare super(...).
Se nella classe base c'è logica importante nel costruttore, ma non la chiamate, il programma può comportarsi in modo imprevisto o addirittura non compilare.
Errore n. 2: avete sovrascritto il metodo sbagliato.
Volevate cambiare il comportamento del metodo del genitore, ma in realtà avete aggiunto un nuovo metodo con un nome simile o parametri diversi. Risultato: il vecchio metodo continua a funzionare come prima, e il vostro nuovo non viene mai chiamato.
Errore n. 3: avete provato a sovrascrivere un metodo final.
Java non ve lo permetterà, ed è un bene! Ma se vedete un errore di compilazione — cercate final.
Errore n. 4: avete sovraccaricato il metodo fino a renderlo irriconoscibile.
Quando avete 10 varianti del metodo calculate, e vi confondete voi stessi su quale venga chiamata — è ora di fermarsi e pensare a un refactoring.
Errore n. 5: avete violato il principio di Liskov.
Se la vostra sottoclasse cambia il significato del comportamento della classe base, l'intera architettura può "deragliare". Per esempio, se avete una classe Shape con il metodo getArea(), e la sottoclasse BrokenShape restituisce -1, questo può portare a bug strani.
GO TO FULL VERSION