Polimorfismo
Last updated
Last updated
Riprendendo l'esempio sul 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).
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.
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.
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".
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.
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:
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:
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
di Persona
).
creo una classe di Prodotti
con attributi nomeProdotto
di tipo String e un altro prezzo
di tipo float; usando sempre it.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
.
costruite una nuova classe che implementa Comparator
per ordinate oggetti di tipo Persona
in base al nome, invece che per l'altezza;
costruite un nuovo Comparator
per Prodotti
e ordinateli per il prezzo;
Cominciamo dalla signature del metodo Collections.sort:
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.
TO-DO: Costruire un esempio simile usando la classe java.util.Arrays
e utilizzando il metodo sort() per l'ordinamento degli array.
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).
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.
Codice esempio .
Vedi esempio: il codice nel package it.esempiosorter.comparable
.
Siccome le problematiche di ordinamento (sorting) sono molto comuni nella programmazione, Java nelle librerie standard (JDK - Java Development Kit), giร fornisce le classi e con una serie di metodi static sort per ordinare rispettivamente array e collezioni. Fornisce inoltre le interfacce (java.lang.Comparable) e (java.util.Comparator) come quelle da noi prima da noi implementate.
La signature del metodo per ora รจ troppo complicata da capire completamente. Dice in sostanza che gli elementi della lista devono implementare l'interfaccia che ha un solo metodo:
Vedi esempio: il codice nel package it.esempiosorter.general
.
Vedi esempio: . Completare il codice implementatndo cil metodo compareTo.