1. Polimorfismo nelle collezioni: a cosa serve?
Iniziamo con una domanda: «A cosa serve davvero il polimorfismo nelle applicazioni reali?»
Immaginate uno zoo. Avete una classe base Animal e un sacco di sottoclassi: Dog, Cat, Cow, Parrot e persino Platypus (ornitorinco, per gli amanti dell’esotico). Ognuno sa emettere un suono (makeSound()), ma lo fa a modo suo.
Invece di creare array separati per ogni animale, dichiarate un array o una lista di tipo Animal e ci mettete dentro chi volete:
Animal[] animals = {
new Dog(),
new Cat(),
new Cow(),
new Parrot()
};
Ora potete scorrere questo array e chiamare makeSound() per ognuno:
for (Animal animal : animals) {
animal.makeSound();
}
Magia! Ogni oggetto sa da sé quale suono emettere e non dovete scrivere una marea di if o switch.
Esempio con un’analogia
È come se diceste «Voce!» a un gruppo di animali, e ognuno decidesse da solo cosa fare: il cane abbaia, il gatto miagola e la mucca — muggirà. Non specificate chi è chi — chiamate semplicemente lo stesso metodo.
2. Esempio pratico: gerarchia dei dipendenti
Rendiamo l’esempio più vicino alla realtà (e al vostro futuro lavoro nell’IT). Immaginate un’azienda con diversi dipendenti: manager, sviluppatori, tester. Ognuno ha un metodo work(), ma viene eseguito in modo diverso.
Dichiarazione della classe base
public class Employee {
public void work() {
System.out.println("Il dipendente lavora...");
}
}
Sottoclassi
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Il manager tiene una riunione.");
}
}
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Lo sviluppatore scrive codice.");
}
}
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Il tester cerca bug.");
}
}
Uso di un array/lista del tipo base
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work(); // Verrà chiamata la versione “corretta” del metodo per ogni oggetto
}
}
}
Risultato dell’esecuzione:
Il manager tiene una riunione.
Lo sviluppatore scrive codice.
Il tester cerca bug.
Lo sviluppatore scrive codice.
Quali sono i vantaggi?
- Non scrivete un mucchio di controlli del tipo «Se è Developer — fai questo e quello».
- Aggiungere un nuovo dipendente (per esempio, Designer) — basta creare una nuova classe e aggiungerlo all’array.
- Il codice che usa l’array dei dipendenti non cambia affatto!
3. Vantaggi del polimorfismo: flessibilità ed estendibilità
Immaginate che nella vostra azienda compaia un nuovo tipo di dipendente — Designer. Tutto ciò che dovete fare è creare una nuova classe:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Il designer disegna mockup.");
}
}
Ora si può aggiungere il designer al team:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Voilà! Il programma inizia subito a funzionare correttamente con il nuovo tipo di dipendente, senza cambiare una riga del codice che scorre l’array e chiama work().
Questa è l’estendibilità: il vostro codice si adatta facilmente a nuovi tipi di oggetti.
4. Limitazioni del polimorfismo: l’altra faccia della medaglia
Purtroppo, ogni magia ha i suoi limiti (e un prezzo, come in qualsiasi RPG).
Sono disponibili solo i metodi della classe base
Quando lavorate con una variabile di tipo Employee, potete chiamare solo i metodi dichiarati nella classe Employee. Se nella classe Developer esiste un metodo specifico writeCode(), non potrete chiamarlo direttamente:
Employee e = new Developer();
// e.writeCode(); // Errore di compilazione: tale metodo non esiste in Employee!
Se proprio volete chiamare un metodo specifico, dovrete fare un cast. Ma è l’ultima risorsa. Se fate spesso cast, forse dovreste rivedere il design delle classi — la classe base o l’interfaccia dovrebbe contenere il metodo necessario.
if (e instanceof Developer) {
Developer dev = (Developer) e;
dev.writeCode();
}
Ma così perdete l’universalità e l’eleganza per cui tutto questo è stato pensato. Quindi cercate di progettare la classe base in modo che contenga solo quei metodi che servono davvero a tutte le sottoclassi.
5. Pratica: realizziamo una gerarchia di dipendenti
Uniremo l’utile al dilettevole: scriviamo una semplice applicazione in cui ci sono diversi tipi di dipendenti e usiamo il polimorfismo per gestirli.
Passo 1: classe base e sottoclassi
// Employee.java
public class Employee {
public void work() {
System.out.println("Il dipendente lavora...");
}
}
// Manager.java
public class Manager extends Employee {
@Override
public void work() {
System.out.println("Il manager tiene una riunione.");
}
}
// Developer.java
public class Developer extends Employee {
@Override
public void work() {
System.out.println("Lo sviluppatore scrive codice.");
}
}
// Tester.java
public class Tester extends Employee {
@Override
public void work() {
System.out.println("Il tester cerca bug.");
}
}
Passo 2: classe principale
// CompanyDemo.java
public class CompanyDemo {
public static void main(String[] args) {
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Developer()
};
for (Employee e : team) {
e.work();
}
}
}
Passo 3: aggiungiamo estendibilità
Supponiamo che tra un mese in azienda compaia un nuovo dipendente — un designer. Ecco tutto ciò che serve:
public class Designer extends Employee {
@Override
public void work() {
System.out.println("Il designer disegna mockup.");
}
}
Fatto, ora si può aggiungere il designer al team:
Employee[] team = {
new Manager(),
new Developer(),
new Tester(),
new Designer()
};
Conclusione
Tutto il codice principale (CompanyDemo) è rimasto invariato! Questa è la forza del polimorfismo.
Errori tipici nell’uso del polimorfismo
Errore n° 1: aspettarsi di accedere a metodi specifici tramite un riferimento del tipo base.
Molto spesso i principianti cercano di chiamare metodi specifici della sottoclasse tramite una variabile del tipo superclasse. Per esempio:
Employee e = new Developer();
// e.writeCode(); // Errore! Questo metodo non è definito in Employee.
Per chiamare un metodo specifico bisogna fare un cast, ma così si perde l’universalità.
Errore n° 2: non usare l’annotazione @Override.
Se vi dimenticate l’annotazione, potreste scrivere accidentalmente un metodo nuovo invece di sovrascriverne uno (per esempio, sbagliando nome). In tal caso il polimorfismo semplicemente non funzionerà e verrà chiamata la versione della superclasse.
Errore n° 3: assenza di un’interfaccia comune.
Se la classe base non contiene il metodo necessario, il polimorfismo non è possibile. Per esempio, se in Employee non c’è il metodo work(), allora il ciclo sull’array dei dipendenti non potrà chiamarlo per tutti.
Errore n° 4: violazione del principio di apertura/chiusura.
Se per aggiungere un nuovo tipo di dipendente bisogna modificare il codice che scorre l’array/lista, significa che non state usando correttamente il polimorfismo.
GO TO FULL VERSION