Polimorfismo

Riprendendo l'esempio sul Polimorfismo già visto nel capitolo precedente (esempio Instrument). Rivediamo il metodo tune():

public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
}

public static void main(String[] args) {
    Wind flute = new Wind();
    Stringed violin = new Stringed();
    Brass frenchHorn = new Brass();
    tune(flute); // No upcasting
    tune(violin);
    tune(frenchHorn);
}

L' output è:

Wind.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C

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ì:

Shape s = new Circle();

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):

s.draw();

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:

package net.mindview.util;

public class Print {
	
	public static void print(String message) {
		System.out.println(message);
	}
}
package polymorphisme.shape;

public class Shape {
	public void draw() {
	}

	public void erase() {
	}
}
package polymorphisme.shape;

import static net.mindview.util.Print.*;

public class Circle extends Shape {
	public void draw() {
		print("Circle.draw()");
	}

	public void erase() {
		print("Circle.erase()");
	}
}
package polymorphisme.shape;

import static net.mindview.util.Print.*;

public class Square extends Shape {
	public void draw() {
		print("Square.draw()");
	}

	public void erase() {
		print("Square.erase()");
	}
}
package polymorphisme.shape;

import static net.mindview.util.Print.*;

public class Triangle extends Shape {
	public void draw() {
		print("Triangle.draw()");
	}

	public void erase() {
		print("Triangle.erase()");
	}
}
package polymorphisme.shape;

import java.util.*;

public class RandomShapeGenerator {
	private Random rand = new Random(47);

	public Shape next() {
		switch (rand.nextInt(3)) {
		default:
		case 0:
			return new Circle();
		case 1:
			return new Square();
		case 2:
			return new Triangle();
		}
	}
}
package polymorphisme.shape;

public class Shapes {
	private static RandomShapeGenerator gen = new RandomShapeGenerator();

	public static void main(String[] args) {
		Shape[] s = new Shape[9];
		// Fill up the array with shapes:
		for (int i = 0; i < s.length; i++)
			s[i] = gen.next();
		// Make polymorphic method calls:
		for (Shape shp : s)
			shp.draw();
	}
}

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:

class Useful {
    public void f() {}
    public void g() {}
}

class MoreUseful extends Useful {
    public void f() {}
    public void g() {}
    public void u() {}
    public void v() {}
    public void w() {}
}

public class RTTI {
    public static void main(String[] args) {
        Useful[] x = {
                new Useful(),
                new MoreUseful()
                };
        x[0].f();
        x[1].g();
        // Compile time: method not found in Useful:
        //! x[1].u();
        ((MoreUseful)x[1]).u(); // Downcast/RTTI
        ((MoreUseful)x[0]).u(); // Exception thrown
        }
}

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:

package it.esempiosorter.comparable;

public interface Comparable {
	
	/**
	* Compares this object with the specified object for order. 
	* Returns a negative integer, zero, or a positive integer as this object 
	* is less than, equal to, or greater than the specified object.
	**/
	public int compareTo(Object o);
	
}

La classe per il sorting Sorter con il metodo statico sort, che implementa l'algoritmo di sorting (quicksort) su oggetti di tipo Comparable:

package it.esempiosorter.comparable;

public class Sorter {
	
	public static void sort(Comparable[] a, 
            boolean up) {
		sort(a, 0, a.length -1, up);
	}

	private static void sort(Comparable[] a, 
            int from, int to, 
            boolean up) {
		
		// If there is nothing to sort, return
	    if ((a == null) || (a.length < 2)) return;
	    
	    // This is the basic quicksort algorithm, stripped of frills that can make
	    // it faster but even more confusing than it already is.  You should
	    // understand what the code does, but don't have to understand just 
	    // why it is guaranteed to sort the array...
	    // Note the use of the compare() method of the Comparer object.
	    int i = from, j = to;
	    Comparable center = a[(from + to) / 2];
	    do {
	      if (up) {  // an ascending sort
	        while((i < to) && center.compareTo(a[i]) > 0) i++;
	        while((j > from) && center.compareTo(a[j]) < 0) j--;
	      } else {   // a descending sort
	        while((i < to) && center.compareTo(a[i]) < 0) i++;
	        while((j > from) && center.compareTo(a[j]) > 0) j--;
	      }
	      if (i < j) { 
	    	  Comparable tmp = a[i];  a[i] = a[j];  a[j] = tmp;          // swap elements
	       
	      }
	      if (i <= j) { i++; j--; }
	    } while(i <= j);
	    if (from < j) sort(a, from, j, up); // recursively sort the rest
	    if (i < to) sort(a, i, to, up);
		
	}
}

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:

package it.esempiosorter.comparable;

public class Persona implements Comparable {
	
	private final String name;
	private final int altezza;
	
	public Persona(String name, int altezza) {
		this.name = name;
		this.altezza = altezza;
	}

	public String getName() {
		return name;
	}

	public int getAltezza() {
		return altezza;
	}
	
	@Override
	public int compareTo(Object o) {
		Persona pers = (Persona) o;
		if(this.altezza > pers.altezza)
			return 1;
		if(this.altezza == pers.altezza)
			return 0;
		
		return -1;
	}

	@Override
	public String toString() {
		return "Persona [name=" + name + ", altezza=" + altezza + "]";
	}
}

Classe di test:

package it.esempiosorter.comparable;

import java.util.Arrays;

public class TestSorter {

	public static void main(String[] args) {
		
		Persona[] persone = {
				new Persona("Massimo", 175),
				new Persona("Luca", 159),
				new Persona("Chiara", 120),
				new Persona("Stefano", 180),
				new Persona("Eliseo", 160)
		};
		
		System.out.println("PRIMA: " + Arrays.toString(persone));
		
		Sorter.sort(persone, true);
		
		System.out.println("DOPO: " + Arrays.toString(persone));
		
	}
}

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 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:

package it.esempiosorter.comparator;

public final class Persona {
	
	private final String name;
	private final int altezza;
	
	public Persona(String name, int altezza) {
		this.name = name;
		this.altezza = altezza;
	}

	public String getName() {
		return name;
	}

	public int getAltezza() {
		return altezza;
	}
	
	@Override
	public String toString() {
		return "Persona [name=" + name + ", altezza=" + altezza + "]";
	}

}

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:

package it.esempiosorter.comparator;

public interface Comparator {
	
	/**
	* Compares its two arguments for order. 
	* Returns a negative integer, zero, or a positive integer as the first argument 
	* is less than, equal to, or greater than the second.
	**/
	public int compare(Object o1, Object o2);
}

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:

package it.esempiosorter.comparator;

public class Sorter {
	
	public static void sort(Object[] a, 
            boolean up, Comparator comparator) {
		sort(a, 0, a.length -1, up, comparator);
	}
	
	private static void sort(Object[] a, 
            int from, int to, 
            boolean up, Comparator comparator) {
		
		// If there is nothing to sort, return
	    if ((a == null) || (a.length < 2)) return;
	    
	    // This is the basic quicksort algorithm, stripped of frills that can make
	    // it faster but even more confusing than it already is.  You should
	    // understand what the code does, but don't have to understand just 
	    // why it is guaranteed to sort the array...
	    // Note the use of the compare() method of the Comparer object.
	    int i = from, j = to;
	    Object center = a[(from + to) / 2];
	    do {
	      if (up) {  // an ascending sort
	        while((i < to) && comparator.compare(center, a[i]) > 0) i++;
	        while((j > from) && comparator.compare(center, a[j]) < 0) j--;
	      } else {   // a descending sort
	        while((i < to) && comparator.compare(center, a[i]) < 0) i++;
	        while((j > from) && comparator.compare(center, a[j]) > 0) j--;
	      }
	      if (i < j) { 
	    	  Object tmp = a[i];  a[i] = a[j];  a[j] = tmp;          // swap elements
	       
	      }
	      if (i <= j) { i++; j--; }
	    } while(i <= j);
	    if (from < j) sort(a, from, j, up, comparator); // recursively sort the rest
	    if (i < to) sort(a, i, to, up, comparator);
		
	}
}

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:

package it.esempiosorter.comparator;

public class PersonaComparator implements Comparator {

	@Override
	public int compare(Object o1, Object o2) {
		Persona p1 = (Persona) o1;
		Persona p2 = (Persona) o2;
		
		if(p1.getAltezza() > p2.getAltezza())
			return 1;
		
		if(p1.getAltezza() == p2.getAltezza())
			return 0;
		
		return -1;
	}

}

Una classe per testare il sort:

package it.esempiosorter.comparator;

import java.util.Arrays;

public class TestSorter {
	
	public static void main(String[] args) {
		
		Persona[] persone = {
				new Persona("Massimo", 175),
				new Persona("Luca", 159),
				new Persona("Chiara", 120),
				new Persona("Stefano", 180),
				new Persona("Eliseo", 160)
		};
		
		System.out.println("PRIMA: " + Arrays.toString(persone));
		
		Sorter.sort(persone, true, new PersonaComparator());
		
		System.out.println("DOPO: " + Arrays.toString(persone));
		
	}
}

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 tipo Persona in base al nome, invece che per l'altezza;

  • costruite un nuovo Comparator per Prodotti 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:

public static <T extends Comparable<? super T>> void sort(List<T> list)

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:

int compareTo(T o)

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