Un riferimento, tanti tipi
Il polimorfismo è il frutto dell'ereditarietà e delle interfacce. Significa che una sola variabile con il tipo di un padre (o di un'interfaccia) può contenere un oggetto di qualsiasi sottotipo, e quando chiami un suo metodo Java esegue la versione della classe reale dell'oggetto, non quella suggerita dal tipo dichiarato della variabile.
Hai già visto la base nella pagina sull'ereditarietà: una sottoclasse sovrascrive un metodo del padre. Il polimorfismo è ciò che rende utile quell'override: ti permette di scrivere codice per il tipo generale e di lasciare che ogni oggetto concreto si comporti a modo suo.
Sia a sia b sono dichiarati come Animal, eppure ciascuno stampa il proprio verso. La scelta avviene a runtime in base all'oggetto reale.
Dispatch dinamico dei metodi
Il meccanismo dietro a tutto questo è il dispatch dinamico dei metodi: per un metodo di istanza sovrascritto, la JVM guarda la classe dell'oggetto a runtime per decidere quale implementazione chiamare. Il compilatore controlla solo che il metodo esista nel tipo dichiarato; la scelta vera e propria viene rimandata a quando il programma è in esecuzione.
È questo che permette a un solo ciclo di gestire un insieme misto di tipi senza mai chiedere che cosa sia ciascuno:
Il ciclo conosce solo Shape. Se più avanti aggiungi un Triangle extends Shape, questo codice continua a funzionare senza modifiche: è proprio questo il punto. Il codice dipende dall'astrazione, non dall'elenco concreto dei tipi.
Upcasting e downcasting
Salvare un Dog in una variabile Animal è un upcasting: si sale nella gerarchia verso un tipo più generale. È sempre sicuro e Java lo fa in modo implicito, perché ogni Dog è un Animal.
Andare nella direzione opposta è un downcasting: prendi un riferimento al padre e lo tratti come un sottotipo specifico. È una forma di type casting tra tipi riferimento, ed è valida solo se l'oggetto è davvero di quel sottotipo, quindi devi scrivere il cast in modo esplicito e rischi una ClassCastException se sbagli:
L'ultimo cast compila senza problemi, perché il compilatore non può dimostrare che è sbagliato, ma esplode quando viene eseguito, perché un Cat non è un Dog. Non fare mai un downcast sulla fiducia.
Proteggere i downcast con instanceof
Prima di un downcast, controlla il tipo reale con instanceof. Il Java moderno ti permette di legare il risultato nella stessa espressione (pattern matching per instanceof), così eviti il cast separato:
instanceof restituisce false per null, quindi il controllo ti protegge anche da una NullPointerException. Detto questo, se ti ritrovi a scrivere lunghe catene di instanceof, spesso è segno che quel comportamento appartiene alle classi stesse come metodo sovrascritto: lascia che sia il polimorfismo a scegliere il ramo al posto tuo.
Overriding e overloading
Questi due termini si somigliano ma non hanno niente in comune, e confonderli è una fonte classica di dubbi.
L'overriding è una sottoclasse che sostituisce un metodo del padre con la stessa identica firma. Si risolve a runtime in base al tipo dell'oggetto: è il polimorfismo che abbiamo usato finora.
L'overloading è una classe che ha più metodi con lo stesso nome ma liste di parametri diverse. Si risolve in fase di compilazione in base ai tipi degli argomenti, senza alcun dispatch a runtime:
Il compilatore sceglie il describe corrispondente basandosi solo sul tipo statico dell'argomento. Non ci sono oggetti padre e figlio in gioco, quindi questo non è polimorfismo a runtime: è solo il riutilizzo di un nome di metodo.
Una trappola comune: i campi non sono polimorfici
Solo i metodi di istanza vengono scelti a runtime. I campi e i metodi statici si risolvono in base al tipo dichiarato, e questo confonde molte persone:
p.name() esegue la versione di Child (polimorfismo), ma p.label legge il campo di Parent, perché i campi vengono nascosti, non sovrascritti. La soluzione è semplice: tieni i campi private e accedi a loro solo tramite metodi, così vince sempre la chiamata polimorfica.
Prossima pagina: modificatori di accesso
Il polimorfismo funziona bene solo quando le sottoclassi possono vedere e sovrascrivere i membri giusti, mentre il resto del codice non può entrare e rompere le invarianti. Questo equilibrio è controllato dagli accessi public, protected, private e package-private: i modificatori di accesso, nella prossima pagina.
Domande frequenti
Cos'è il polimorfismo in Java?
Polimorfismo significa che un solo tipo riferimento può puntare a oggetti di tante classi diverse, e il metodo che viene eseguito davvero è scelto a runtime in base al tipo reale dell'oggetto, non al tipo dichiarato della variabile. Così una variabile Shape shape può contenere un Circle o un Square, e chiamare shape.area() esegue automaticamente la versione giusta.
Qual è la differenza tra overriding e overloading in Java?
L'overriding si ha quando una sottoclasse sostituisce un metodo della superclasse con la stessa firma: è ciò che rende possibile il polimorfismo a runtime. L'overloading si ha quando una classe ha più metodi con lo stesso nome ma liste di parametri diverse; il compilatore ne sceglie uno in fase di compilazione in base agli argomenti. L'overriding si risolve a runtime in base al tipo dell'oggetto; l'overloading in fase di compilazione in base ai tipi degli argomenti.
Qual è la differenza tra upcasting e downcasting in Java?
L'upcasting tratta un oggetto figlio come il suo tipo padre (Animal a = new Dog();): è sempre sicuro e di solito implicito. Il downcasting va nella direzione opposta (Dog d = (Dog) a;) ed è sicuro solo se l'oggetto è davvero di quel sottotipo; altrimenti lancia ClassCastException. Proteggi sempre ogni downcast con un controllo instanceof.