Polimorfismo
Riprendendo l'esempio sul Polimorfismo già visto nel capitolo precedente (esempio Instrument). Rivediamo il metodo tune()
:
L' output è:
Esso riceve una reference a Instrument. Così come fa il compilatore a sapere che questa reference a Instrument punta a un oggetto di tipo Wind e non a un Brass o Stringed? Il compilatore non può saperlo. Per avere una comprensione più approndita di questa problematica dobbiamo parlare del meccanismo del binding (associazione tra chiamata del metodo ed esecuzione).
Binding della chiamata dei metodi
La connessione tra la chiamata di un metodo e il corpo del metodo, è detta binding. Quando il binding è eseguito prima del'esecuzione del programma (dal compilatore o dal linker) è detto early binding. Può darsi che non ne abbiate mai sentito parlare perchè nei linguaggi procedurali come il C, è la sola opzione.
La parte che confonde del precedente programma è legata all'early binding, perchè il compilatore non può sapere il metodo corretto da chiamare quando ha solo la reference a Instrument.
La soluzione è chiamata late binding, che significa che il binding avviene a run time (durante l'esecuzione), basandosi sul tipo dell'oggetto. Il late binding è anche detto dinamic binding o runtime binding. Quando un linguaggio implementa il late binding, ci deve essere un meccanismo per determinare a runtime il tipo dell'oggetto e chiamare il metodo appropriato. Cioè, il compilatore non può conoscere il tipo dell'oggetto, ma il meccanismo di chiamata del metodo riesce a saperlo e chiama il codice del metodo appropriato. Il meccanismo del late binding, varia da linguaggio a linguaggio, ma si può pensare che qualche tipo di informazione sul tipo sia all'interno dell'oggetto.
Il binding di tutti i metodi in Java è tipo late binding a meno che il metodo non sia static o final (i metodi private sono implicitamente final).
Perchè dichiarare un metodo final? Previene da fare l'overridding del metodo. Forse ancora più importante, disattiva il late binding o piuttosto dice al compilatore che il dinamic binding non è necessario. Permette al compilatore di generare codice leggermente più efficiente.
Produrre il giusto comportamento
Una volta che sappiamo che che il binding di tutti i metodi in Java avviene tramite il late binding, possimo scrivere il codice che parla alle classi base (superclasse, classe astratta o interface) e sapere che tutte le classi derivate lavoreranno correttamente utilizzando lo stesso codice. O detto con altre parole, tu "invii un messaggio a un oggetto ed è poi l'oggetto a capire la cosa giusta da fare".
Con il solito esempio, il diagramma dell'ereditarietà della classe:
Shape
è la classe base e Circle
, Square
e Triangle
sono le classi derivate o sottoclassi.
Facciamo l'upcast, della sottoclasse alla classe base, così:
Con questa operazione, un oggetto di tipo Circle è creato, e la reference risultante è assegnata a una reference Shape, che potrebbe sembrare un errore (l'assegnamento di un tipo a un altro); ma è corretto perché un oggetto di tipo Circle è anche un oggetto di tipo Shape per ereditarietà. Così il compilatore non segnala messaggi d'errore.
Supponiamo che noi chiamiamo un metodo della base class (che noi abbiamo sovrascritto, overrdidden, nella classe derivata):
Di nuovo, noi ci aspetteremmo che il metodo draw() di Shape è invocato perchè dopo tutto abbiamo una reference a Shape, così come potrebbe il compilatore fare altro? Ma vediamo che draw() di Circle è chiamata a causa dal late binding (polimorfismo).
Vediamo il seguente esempio, sempre sul polimorfimo, che mostra come la scelta del metodo da invocare, sia fatta a run-time:
Il metodo next()
della classe RandomShapeGenerator
, restituisce un oggetto sottoclasse di Shape generato in modo casuale (random). L'uppercast è fatto col return del metodo, in quanto il metodo next()
ritorna un oggetto di tipo Shape
. Nel main
di Shapes
viene così riempito un array di oggetti di sottoclassi di Shape
generati in modo casuale. Dall'esecuzione si vede che viene chiamato il metodo draw()
delle sottoclassi. E' come se gli oggetti su cui viene invocato il metodo, conservino memoria di quale sia il loto effettivo tipo.
Codice esempio PolimorfismoRandomShapes.
Estensibilità
Vediamo come il Polimorfismo renda semplice lavorare con gerarchie di classi che si estendono, cioè che aggiungo nuove sottoclassi di una classe base o di una sottoclasse.
Tutte questa nuove classi lavorano correttamente con metodo tune() precedente: non è necessario fare nessuna modifica al metodo e il metodo funziona correttamente anche con le nuove classi aggiunte, Woodwind e Brass. Se il metodo tune() è in un file separato e sono aggiunti dei metodi all'interfaccia Instrument, tune continua a lavorare correttamente, anche senza la necessità di ricompilare. Il polimorfismo è una tecnica importante per "separare le parti che cambiano dalle parti che rimangono le stesse".
Sostituzione pura vs Estensione
Può sembrare che il modo più pulito per creare una gerarchia di ereditarietà è l'approccio "puro". Cioè, solo i metodi che sono stati definiti nella classe base (superclasse) sono sovrascritti nelle classi derivate (sottoclassi), come si vede nel diagramma:
Questo può essere chiamato una relazione "is-a" pura: oggetti della classe base e sottoclassi hanno la stessa interfaccia (cioè gli stessi metodi, anche se sovrascritti nelle sottoclassi).
Questo può essere pensato come sostituzione pura, perchè oggetti delle classi derivate possono essere dei perfetti sostituti per la classe base, e non dobbiamo sapere nessuna informazione extra sulle sottoclassi quando le utilizziamo:
Cioè, la classe base può ricevere qualsiasi messaggio, che può essere inviato alle sottoclassi senza problemi perchè hanno esattamente la stessa interfaccia. Tutto quello che bisogna fare, è l'upcast dalla classe derivata e non preoccuparti più di quale specifico tipo avevi a che fare. Ogni cosa è gestita tramite il polimorfismo.
Quando si vedono le cose in questo modo, sembra che la relazione di sostutuzione pura sia l'unico modo per lavorare. Ma ciò è parziale, vediamo, come anche la parola extends sembra incoraggiare per le sottoclassi, ci possono essere anche relazioni del tipo is-like-a, nel senso che la classe derivata è come la classe base, ha la stessa interfaccia fondamentale, ma ha anche altre funzionalità che necessitano di altri metodi che le implementino:
Sebbene questo è un approccio utile c'è anche un inconveniente: riguardo la parte estesa dell'interfaccia della sottoclasse, una volta fatto l'upcasting alla classe base, non possono essere chiamati i metodi (dell'interfaccia estesa):
Ci sono casi in cui necessiti chiamare i metodi dell'intaccia estesa e devi recuperare il tipo esatto dell'oggetto, non ti basta più la base class ma ti serve come tipo della sottoclasse. Nella sezione seguente vediamo come si fa.
Downcasting e runtime type information
Siccome facendo l'upcasting abbiamo perso le specifiche informazioni del tipo, ha senso che per ritrovare le informazioni del tipo - coiè per scendere nella gerarchia dei tipi - bisogna fare il downcast. Tuttavia sappiamo che l'upcast è sempre sicuro, perchè la classe base non può avere un'interfaccia più grande delle proprie classi derivate. Quindi ogni messaggio inviato tramite la classe base è garantito che sia accettato. Ma con il downcast, noi non sappiamo se una Shape
è (per esempio) un Circle
, potrebbe essere un Triangle
o uno Square
o qualche altra forma.
In Java, ogni cast è controllato a runtime. Se non è corretto, viene sollevata l'eccezione ClassCastException. L' azione di controllare il tipo a runtime è detta runtime type identification (RTTI). Il seguente codice mostra il comportamento di RTTI:
Costruzione di un generic Sorter
Utilizzando il polimorfismo possiamo creare un generic Sorter che ordini un array di oggetti di classi che implementano una interfaccia da noi definita chiamata it.esempiosorter.comparable.Comparable
con un metodo compareTo
. Il metodo decidiamo che ritorni l'intero 0 se i due oggetto per l'ordinamento sono uguali, un mumero > 0 se invece l'oggetto che implementa l'interfaccia è maggiore dell'oggetto passato per il confronto, un numero < 0, viceversa. Questo serve affinchè poi nel metodo che esegue il sorting, ci sia un criterio per controntare due oggetti e dire quale viene prima e dopo in base a come li vogliamo ordinare.
Sotto la nostra interfaccia Comparable
:
La classe per il sorting Sorter
con il metodo statico sort
, che implementa l'algoritmo di sorting (quicksort) su oggetti di tipo Comparable
:
Come si vede il metodo sort ordina l'array di oggetti che implementano l'interfaccia Comparable
: per eseguire l'ordinamento il metodo sort
utilizza il compareTo
quando deve decidere tra due oggetti quale è il meggiore. Il fatto che gli oggetti da ordinare implementino l'interfaccia Comparable
, gli garantisce che il metodo compareTo
è implementato.
Ora sappiamo che, per il polimorfismo, array di oggetti di classi che implementano Comparable
sono sostituibili ad array di Comparable
: basta che la mia classe implementi Comparable
e la posso utilizzare il metodo di sort
da noi implementato.
Vediamo un esempio di utilizzo con oggetti concreti: ordiniamo un array di oggetti di tipo Persona
che implementa l'interfaccia Comparable
:
Classe di test:
Vedi esempio: Polimorfismo Comparable il codice nel package it.esempiosorter.comparable
.
ESERCIZI:
e se volessi ordinare le persone in base al nome della persona e non come ora in base all'altezza? Cosa dovrei fare? Fate una versione che ordina le persone in base al nome. (Suggerimento, dovete modificare il metodo
compareTo
diPersona
).creo una classe di
Prodotti
con attributinomeProdotto
di tipo String e un altroprezzo
di tipo float; usando sempreit.esempiosorter.comparable.Sorter.sort()
implementate l'ordinamento (sorting) in base al prezzo del prodotto.
Vediamo ora un altro caso: ammettiamo che io abbia invece una classe Persona
che non implementa it.esempiosorter.comparable.Comparable
perché ad esempio la classe è stata scritta da qualcun altro ed è final, quindi non posso estendere la classe originale con una sottoclasse che implementi l'interfaccia Comparable
e quindi non posso utilizzare il metodo void sort(Comparable[] a, boolean up)
di it.esempiosorter.comparable.Sorter
. Come fare a questo punto?
Partiamo da una nuova classe Persona
che questa volta non implementa Comparable
:
Scriviamo ora un'altra funzione sort generica che ordina oggetti ma che ha come parametro anche un oggetto che imprementa l'interfaccia Comparator
(da non confondere con Comparable
di prima): la signature del metodo sort: void sort(Object[] a, boolean up, Comparator comparator)
Vediamo l'interfaccia Comparator
:
l'interfaccia ha il solo metodo, compare
, che permette di confrontare due oggetti per dire quale dei due viene prima in base all'ordinamento che vogliamo eseguire: se i due oggetti sono uguali torna il valore 0, se il primo oggetto è maggiore del secondo allora ritorna un numero > 0 altrimenti un numero < 0. Il criterio per confrontare due oggetti è in questo caso passato come parametro alla funzione sort come oggetto che implementa l'interfaccia Comparator
.
Vediamo il nuovo sorter per oggetti generici:
L'implementazione è molto simile al sort precedente: si può vedere che nella signature del metodo ora accetta array di tipo Object
, ma viene passato anche un oggetto che implementa l'interfaccia Comparator
che è utilizzato per confrontare due oggetti e dire quale dei due viene prima nell'ordinamento.
Vediamo quindi di implementare un Comparator
per oggetti di tipo Persona
per ordinarle per attributo eta
:
Una classe per testare il sort:
Il fatto che sort abbia per parametro un array di Object
permette di invocare il metodo anche per array di qualsiasi altra classe in quanto sottoclasse di Object
: nel nostro caso come array di oggetti di tipo Persona
.
ESERCIZI:
costruite una nuova classe che implementa
Comparator
per ordinate oggetti di tipoPersona
in base al nome, invece che per l'altezza;costruite un nuovo
Comparator
perProdotti
e ordinateli per il prezzo;
Esempio polimorfismo utilizzato in classi JDK - Collections.sort()
Siccome le problematiche di ordinamento (sorting) sono molto comuni nella programmazione, Java nelle librerie standard (JDK - Java Development Kit), già fornisce le classi Arrays e Collections con una serie di metodi static sort per ordinare rispettivamente array e collezioni. Fornisce inoltre le interfacce Comparable (java.lang.Comparable) e Comparator (java.util.Comparator) come quelle da noi prima da noi implementate.
Cominciamo dalla signature del metodo Collections.sort:
La signature del metodo per ora è troppo complicata da capire completamente. Dice in sostanza che gli elementi della lista devono implementare l'interfaccia Comparable che ha un solo metodo:
Se gli oggetti della lista che voglio ordinare implementano l'interfaccia Comparable allora il metodo statico Collections.sort() permette di ordinare la lista.
Quindi il metdoto sort sfrutta il polimorfismo, nel senso che le liste passate come parametri, vengono viste come liste di Comparable e utilizzando il metodo di Comparable, compareTo, vengono ordinate.
Vedi esempio: Polimorfismo Comparable il codice nel package it.esempiosorter.general
.
TO-DO: Costruire un esempio simile usando la classe java.util.Arrays
e utilizzando il metodo sort() per l'ordinamento degli array.
Esempio polimorfismo Sistema gestione temperatura
Air Conditioner is-a (è ) un Cooling System (hanno la stessa interfaccia, intesa come metodi pubblici della classe).
Heat Pump is-like-a (è come) un Cooling System (aggiunge il metodo heat() all'interfaccia, intesa come metodi pubblici della classe).
Vedi esempio: EsempioGestioneTemperatura. Completare il codice implementatndo cil metodo compareTo.
RIASSUNTO
Polimorfismo significa "forme differenti". Nella programmazione ad oggetti, abbiamo la stessa interface dalla classe base, e differenti forme usando quella interfaccia: le differenti versioni dei metodi dinamicamente chiamati.
Come aggiamo visto in questo capitolo è impossibile capire o anche creare, un esempio di polimorfismo senza usare l'astrazione dei dati e l'ereditarietà. Il polimorfismo è una proprietà che non può essere vista isolata (come lo switch statement per esempio), ma invece lavora in collaborazione, come parte di un mosaico più grande. delle relazioni tra le classi.
Per utilizzare il polimorfismo - e quindi la modalità di programmazione ad oggetti - bisogna espandere la visione della programmazione per includere non solo attributi e metodi di una singola classe, ma anche le cose in comuni tra le classi e le relazioni tra le classi. Sebbene lo sforzo richiesto non sia da poco, il gioco vale la candela. Il risultato è una modalità di sviluppo più veloce, migliore organizzazione del codice, programmi estensibili, e più facili da mantenere.
Last updated