1. Introduzione
Il polimorfismo — è uno dei tre pilastri della programmazione orientata agli oggetti (insieme a ereditarietà e incapsulamento). Letteralmente deriva dal greco “poly” (molti) e “morph” (forma). In programmazione significa: un’interfaccia — molte implementazioni.
Definizione
Il polimorfismo è la capacità di oggetti di classi diverse di rispondere allo stesso messaggio (chiamate di metodi) in modi differenti.
Cioè, se hai un metodo makeSound(), puoi invocarlo su qualsiasi animale, ma il gatto miagolerà, il cane abbaierà e la mucca muggirà. Per il programmatore è semplicemente la chiamata animal.makeSound(), ma ciò che avverrà realmente dipende da quale oggetto effettivo sta dietro a quella variabile.
Un’analogia dalla vita reale
Immagina di avere a casa un telecomando della TV, e con lo stesso telecomando puoi controllare anche gli altoparlanti, il proiettore e perfino la macchina del caffè. Premi il pulsante “Accendi” — turnOn(), e ogni dispositivo reagisce a modo suo. L’importante è che tutti abbiano il “pulsante” di accensione, ma l’implementazione è diversa.
Esempio in Java
class Animal {
void makeSound() {
System.out.println("Qualche suono...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miao!");
}
}
class Cow extends Animal {
@Override
void makeSound() {
System.out.println("Muu!");
}
}
Ora possiamo fare così:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
Animal animal3 = new Cow();
animal1.makeSound(); // Bau!
animal2.makeSound(); // Miao!
animal3.makeSound(); // Muu!
Nota: tutte le variabili hanno tipo Animal, ma il risultato della chiamata dipende dal tipo effettivo dell’oggetto.
2. Tipi di polimorfismo
In Java (e nella maggior parte dei linguaggi OOP) si distinguono due tipi principali di polimorfismo:
Polimorfismo di compilazione (statico) — sovraccarico dei metodi (overloading)
Si ha quando, in una stessa classe, esistono più metodi con lo stesso nome ma parametri diversi. Il compilatore decide quale metodo invocare in base agli argomenti passati.
Esempio (uno sguardo avanti — più dettagli nella prossima lezione):
class Printer {
void print(int x) {
System.out.println("Numero: " + x);
}
void print(String s) {
System.out.println("Stringa: " + s);
}
}
Polimorfismo di esecuzione (dinamico) — ridefinizione dei metodi (overriding)
Accade quando un metodo è definito nella classe base e poi ridefinito nelle sottoclassi. Quale metodo verrà chiamato viene deciso durante l’esecuzione del programma (runtime), a seconda del tipo effettivo dell’oggetto.
Esempio: vedi sopra con gli animali.
3. Perché serve il polimorfismo?
Il polimorfismo non è solo una bella parola per i colloqui. È uno strumento che rende il tuo codice flessibile, estendibile e facile da mantenere.
Universalità del codice
Puoi scrivere codice che lavora con oggetti del tipo base senza preoccuparti dei dettagli della loro implementazione. Ad esempio, se hai una lista di animali, puoi iterarla e chiamare su ciascuno makeSound(), senza pensare se sia un gatto o un cane.
Animal[] animals = { new Dog(), new Cat(), new Cow() };
for (Animal animal : animals) {
animal.makeSound(); // Ogni volta verrà chiamato il metodo "giusto"
}
Facilità di estensione
Se domani il capo ti dicesse: “Aggiungiamo un pappagallo!”, ti basterebbe scrivere una nuova classe Parrot extends Animal e aggiungerla all’array. Tutto il resto del codice rimarrebbe invariato. Questo è aperti all’estensione e chiusi alla modifica (principio OCP di SOLID).
Semplificazione dell’architettura
Puoi costruire sistemi complessi in cui le parti interagiscono tra loro tramite astrazioni (classi base o interfacce), senza preoccuparsi delle implementazioni concrete. Questo fa risparmiare tempo, nervi e caffè.
4. Concetti chiave: tipo di riferimento e tipo effettivo
Tipo di riferimento della variabile
Quando scrivi Animal animal = new Dog();, la variabile animal ha tipo di riferimento Animal, cioè il compilatore “pensa” che sia un animale e consente solo i metodi dichiarati nella classe Animal.
Tipo effettivo (reale) dell’oggetto
Ma in memoria hai realmente un oggetto di tipo Dog. È proprio questo a determinare quale metodo verrà chiamato quando si invoca makeSound().
Illustrazione
Animal animal = new Dog();
animal.makeSound(); // Richiamerà Dog.makeSound(), non Animal.makeSound()
Importante! Tramite un riferimento del tipo base (Animal) non puoi invocare metodi che esistono solo in Dog, se non sono dichiarati nella classe base.
Binding tardivo (dinamico)
È la “magia” che avviene durante l’esecuzione: quando invochi un metodo tramite un riferimento del tipo base, la JVM guarda il tipo effettivo dell’oggetto e chiama l’implementazione “corretta”. Questo è polimorfismo in azione.
5. Esempio pratico: polimorfismo in un’applicazione
Continuiamo a sviluppare la nostra applicazione didattica. Supponiamo di scrivere una semplice simulazione di uno zoo. Abbiamo una classe base Animal e alcune sue derivate. Vogliamo che tutti gli animali possano “emettere un suono”, ma senza dover scrivere ogni volta codice separato per ogni tipo di animale.
Passo 1: classe base e derivati
class Animal {
void makeSound() {
System.out.println("Qualche suono...");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Bau!");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Miao!");
}
}
Passo 2: array di animali
Animal[] zoo = { new Dog(), new Cat(), new Animal() };
Passo 3: iterazione e invocazione del metodo
for (Animal animal : zoo) {
animal.makeSound();
}
Risultato dell’esecuzione:
Bau!
Miao!
Qualche suono...
Nota: non sappiamo in anticipo chi ci sia effettivamente nell’array — ma il programma se la cava da solo e invoca il metodo corretto.
6. Rappresentazione schematica del polimorfismo
. Animal (makeSound)
/ \
Dog Cat
(makeSound) (makeSound)
Animal animal = new Dog();
animal.makeSound(); // --> Dog.makeSound()
Animal animal = new Cat();
animal.makeSound(); // --> Cat.makeSound()
7. Un altro esempio: polimorfismo in un caso reale
Supponiamo che tu stia scrivendo un programma per la gestione dei dipendenti di un’azienda. Hai una classe base Employee e due derivati: Manager e Developer. Tutti i dipendenti possono lavorare — work(), ma lo fanno in modo diverso.
class Employee {
void work() {
System.out.println("Il dipendente sta lavorando.");
}
}
class Manager extends Employee {
@Override
void work() {
System.out.println("Il manager tiene una riunione.");
}
}
class Developer extends Employee {
@Override
void work() {
System.out.println("Lo sviluppatore scrive codice.");
}
}
Ora puoi fare così:
Employee[] staff = { new Manager(), new Developer(), new Employee() };
for (Employee emp : staff) {
emp.work();
}
Risultato:
Il manager tiene una riunione.
Lo sviluppatore scrive codice.
Il dipendente sta lavorando.
8. Quando il polimorfismo NON funziona
Il polimorfismo funziona solo per i metodi dichiarati nella classe base. Se in una sottoclasse esiste un metodo unico, tramite un riferimento del tipo base non lo vedrai.
class Dog extends Animal {
void fetchStick() {
System.out.println("Il cane riporta il bastone!");
}
}
Animal animal = new Dog();
// animal.fetchStick(); // Errore di compilazione! Un tale metodo non è visibile tramite Animal
Per chiamare un metodo specifico devi effettuare il cast della variabile al tipo necessario:
if (animal instanceof Dog) {
((Dog) animal).fetchStick();
}
Ma questa è un’altra storia — l’importante è: tramite il polimorfismo sono disponibili solo i metodi dichiarati nella classe base.
9. Errori tipici quando si lavora con il polimorfismo
Errore n. 1: Aspettarsi che tramite un riferimento del tipo base siano disponibili tutti i metodi della sottoclasse. In realtà sono disponibili solo quelli dichiarati nella classe base.
Errore n. 2: Mancato uso dell’annotazione @Override nel ridefinire un metodo. Senza di essa si può per sbaglio scrivere un metodo con firma errata, e allora il polimorfismo non funzionerà (il metodo della classe base non verrà ridefinito).
Errore n. 3: Tentare di chiamare un metodo specifico della sottoclasse senza cast. Il compilatore non lo permetterà, perché non sa chi esattamente si nasconde dietro il riferimento del tipo base.
Errore n. 4: Confondere overloading e overriding. L’overloading è avere più metodi con lo stesso nome e parametri diversi nella stessa classe. L’overriding è modificare il comportamento del metodo in una sottoclasse.
GO TO FULL VERSION