Subtype Polymorphism
• Interfacce e subtype polimorfismo
 Tipi, sottotipi e conversioni di tipo
 Polimorfismo e dinamic dispatch
Conversioni di tipo
• Variabile: locazione con un tipo associato
 Tipo della variabile determinato dal compilatore
guardando la dichiarazione
 Una variabile di tipo reference contiene un riferimento
ad un oggetto
• Oggetto: istanza di una classe
 Tipo dell’oggetto: la classe che lo crea
 Determinato a run time
• Una variabile può assumere come valori
riferimenti ad oggetti di classi diverse
Continua…
Conversioni di tipo
• È possibile assegnare un riferimento di tipo
classe ad una variabile di tipo interfaccia
purchè la classe implementi l’interfaccia
BankAccount account = new BankAccount(10000);
Measurable x = account; // OK
Coin dime = new Coin(0.1, "dime");
Measurable y = dime; // OK
Continua…
Conversioni di tipo
• La conversione è lecita solo in determinate
situazioni
Measurable x = new Rectangle(5, 10, 20, 30); // ERRORE
• Problema: Rectangle non implementa
Measurable
Subtyping
• Subtyping (<:)
 Una relazione che permette di decidere quando è
legittimo convertire un tipo riferimento in un altro
• Chi decide cosa/quando è legittimo?
 il compilatore!
• Per il momento la regola è:
T1 <: T2 sse
 T1 è una classe, T2 è una interfaccia
 T1 implementa T2.
Continua…
Subtyping
• Principio di sostituibilità
 Un riferimento di un sottotipo può essere usato
ovunque ci si aspetti un riferimento di un supertipo
• Le regole di sottotipo devono garantire la
correttezza del principio di sostituibilità
Continua…
Subtyping
• La regola
 C <: I se C implementa I
• Principio di sostituibilità
 un riferimento di tipo C può sempre essere usato dove
si attende un riferimento di tipo I
• E’ ragionevole perché
 se C implementa I , C definisce public tutti i metodi
dichiarati da I
 Quindi tutti le invocazioni di metodo possibili per I sono
supportate da C
Continua…
Subtyping e regole di typing
• Regola di assegnamento
 Un riferimento di tipo T1 si puo sempre assegnare ad una variabile
di tipo T2 sse T1 <: T2
 Un riferimento di tipo classe può sempre essere assegnato ad una
variabile di tipo interfaccia (se la classe implementa l’interfaccia)
• Regola di passaggio di parametri
 Un riferimento di tipo T1 si puo sempre passare per un parametro
di tipo T2 sse T1 <: T2
 Un riferimento di tipo classe può sempre essere passato per un
parametro di tipo interfaccia (se la classe implementa l’interfaccia)
Domanda
• Data l’implementazione generica della classe
DataSet, che oggetti possiamo passare
come argomento per x ?
public class DataSet
{
public void add(Measurable x) { ... }
...
}
Risposta
• Qualunque istanza di una una classe che
implementa Measurable
Polimorfismo – dynamic dispatch
• Una variabile di tipo interfaccia ha sempre
come valore un riferimento di una classe che
implementa l’interfaccia
Measurable x;
x = new BankAccount(10000);
x = new Coin(0.1, "dime");
Continua…
Polimorfismo – dynamic dispatch
• Possiamo invocare ognuno dei metodi
dell’interfaccia:
double m = x.getMeasure();
• Quale metodo invoca?
Polimorfismo – dynamic dispatch
• Dipende dal riferimento corrente
memorizzato nella variabile
• Se x riferisce un BankAccount, invoca il
metodo BankAccount.getMeasure()
• Se x riferisce un Coin, invoca il metodo
Coin.getMeasure()
• Polimorfismo (molte forme):
 il comportamento varia, e dipende dal tipo dinamico
della variabile
Continua…
Domande
5. È impossibile costruire un oggetto di tipo
Measurable.Perché?
6. Perché invece é possibile dichiarare una
variabile di tipo Measurable?
Risposte
5. Measurable è una interfaccia. Le
interfacce non hanno campi o
implementazione di metodo.
6. Perché Measurable è un tipo: la variabile
non riferirà mai una istanza di Measurable,
(le interfacce non hanno istanze) ma
piuttosto oggetto di una qualche classe che
implementa l’interfaccia Measurable.
Continua…
Ancora un esempio
•
Costruiamo una applicazione per disegnare
un insieme di forme geometriche contenute
in una componente grafico:


definiamo GWin, una classe che descrive un
contenitore di forme geometriche disegnate
mediante una invocazione del metodo paint()
per esemplificare, consideriamo due tipi di forme:
Car e Smiley
Forme grafiche
class Car
{
. . .
public void draw(Graphics2D g)
{
// Istruzioni per il disegno
. . .
}
}
class Smiley
{
. . .
public void draw(Graphics2D g)
{
// Istruzioni per il disegno
. . .
}
}
GWin
•
Un contenitore di Cars e Smileys
/**
Una finestra che contiene un insieme Cars e Smileys
*/
class GWin
{
/**
Disegna tutte le forme di questo component
*/
public void paint(){ /* disegna su g */ }
/**
Componente grafica su cui disegnare
*/
private Graphics2D g;
}
Domanda
• Che struttura utilizziamo per memorizzare le
forme contenute nella GWin?
• Come definiamo il metodo paint() in modo
che disegni tutte le forme della componente?
Risposte
• definiamo una nuova interfaccia: Shape
interface Shape { void draw(Graphics2D g); }
• Ridefiniamo le classi Car e Smiley in modo
che implementino Shape
• Memorizziamo gli oggetti della componente
in una ArrayList<Shape>
Car e Smiley implementano Shape
class Car implements Shape
{
. . .
public void draw(Graphics2D g)
{
// Istruzioni per il disegno
. . .
}
}
class Smiley implements Shape
{
. . .
public void draw(Graphics2D g)
{
// Istruzioni per il disegno
. . .
}
}
GWin
Mantiene una ArrayList<Shape>
class GWin
{
private Graphics2D g;
private ArrayList<Shape> shapes;
// crea una GWin con un insieme di forme
public GWin(Shape... shapes)
{
Graphics2D g = new Graphics2D();
this.shapes = new ArrayList<Shape>();
for (Shape s:shapes) this.shapes.add(s);
}
// disegna tutte le componenti della GWin
public void paint()
{
for (Shape s:shapes) s.draw(g);
}
}
Diagramma delle Classi
Car
Smiley
GWin
Shape
So long, for today
Polimorfismo – dynamic dispatch
• Dynamic dispatch:
 Il metodo da invocare per rispondere ad un
messaggio è deciso a tempo di esecuzione
Dynamic dispatch in GWin
class GWin
{
. . .
private ArrayList<Shape> shapes;
. . .
public void paint()
{
// disegna tutte le componenti della GWin
// il metodo invocato effettivamente da ogni
// messaggio s.draw(g) dipende dalla classe
// di cui s è istanza ed è deciso a runtime
for (Shape s:shapes) s.draw(g);
}
. . . .
}
Dynamic dispatch vs overloading
• Dynamic dispatch:
 Il metodo da invocare per rispondere ad un
messaggio è deciso a tempo di esecuzione
• Notiamo bene
 Il metodo da invocare è deciso a runtime
 il compilatore decide se esiste un metodo da invocare
• Overloading:
 Nel caso esista più di un metodo, il compilatore
decide staticamente il tipo del metodo da invocare
Dynamic dispatch vs overloading
interface I
{
public String m(boolean b);
public String m(double d);
}
class A implements I
{
public String m(boolean b) { return “A.m(boolean)”; }
public String m(double d) { return “A.m(double)”; }
}
class B implements I
{
public String m(boolean b) { return “B.m(boolean)”; }
public String m(double d) { return “B.m(double)”; }
}
Dynamic dispatch vs overloading
class Client
{
public void static show(I x)
{
// tipo del metodo invocato = m(boolean)
// deciso dal compilatore staticamente
// metodo invocato deciso dinamicamente
// in funzione del tipo dell’argomento
// passato per x
System.out.println( x.m(false) );
}
}
Domanda
•
Che cosa hanno in comune i meccanismi di
overloading e di dynamic dispatch? In cosa
sono diversi?
Risposta
•
Entrambi i meccanismi contribuiscono a
decidono quale metodo eseguire in risposta
ad un messaggio, ma
•
•
Nell’overloading scelta è relativa al tipo del metodo,
ed è fatta in compilazione guardando il tipo dei
parametri
Nel dynamic dispatch la scelta è relativa al corpo del
metodo, ed è fatta in esecuzione guardando il tipo
dell’oggetto che riceve il messaggio
Subtyping e sostituibilità
• Principio di sostituibilità
 Un riferimento di un sottotipo può essere usato
ovunque ci si aspetti un riferimento di un supertipo
• Le regole di sottotipo garantiscono la
correttezza del principio di sostituibilità
 Tutti i metodi del supertipo devono essere
implementati dal sottotipo
 Il sottotipi può avere anche altri metodi
Continua…
Subtyping e perdita di informazione
• Principio di sostituibilità
 Un riferimento di un sottotipo può essere usato
ovunque ci si aspetti un riferimento di un supertipo
• Può causare perdita di informazione
 nel contesto in cui ci aspettiamo il supertipo, non
possiamo usare solo I metodi del supertipo
 perdiamo la possibilità di utilizzare gli eventuali metodi
aggiuntivi del sottotipo
Continua…
Car e Smiley implementano Shape
class Car implements Shape
{
. . .
public void draw(Graphics2D g){ . . . }
public String brand() {. . . }
}
class Smiley implements Shape
{
. . .
public void draw(Graphics2D g){ . . . }
public String mood() {. . . }
}
Subtyping e perdita di informazione
• Consideriamo
public static void printBrand(List<Shape> l)
{
for (Shape s : l)
// stampa la marca di tutte le macchine di l
// ???
}
Continua…
Subtyping e perdita di informazione
• Certamente non possiamo fare così …
public static void printBrand(List<Shape> l)
{
for (Shape s : l)
// stampa la marca di tutte le macchine di l
System.out.println( s.brand() ); // TYPE ERROR!
}
Continua…
Cast
• Permette di modificare il tipo associato ad una
espressione
((Car)s).brand()
• Un cast è permesso dal compilatore solo se applica
conversioni tra tipi compatibili
• Compatibili = sottotipi (per il momento)
• Anche quando permesso dal compilatore, un cast
può causare errore a run time
• Se s non è un Car errore a run time
Continua…
Cast
• Tipo statico e tipo dinamico di una variabile
 tipo statico: quello dichiarato
 tipo dinamico: il tipo del riferimento assegnato alla
variabile
• (T)var causa errore
 in compilazione
se T non è compatibile con il tipo statico di var
 in esecuzione (ClassCastException)
se T non è compatibile con il tipo dinamico di var
Continua…
Cast
Shape s =
new Car();
• OK: Car sottotipo di Shape
Car c = (Car) s
• Compila correttamente
 il tipo dichiarato di s è Shape
 Car e Shape sono compatibili
• Esegue correttamente
 s è un Car (il tipo dinamico di s è Car)
Continua…
Cast
Shape s =
new Car();
• OK: Car sottotipo di Shape
Smiley c = (Smiley) s
• Compila correttamente
 il tipo dichiarato di s è Shape
 Smiley e Shape sono compatibili
• Errore a run time
 s non è uno Smiley
Continua…
Cast
• Attenzione anche qui …
public static void printBrand(List<Shape> l)
{
for (Shape s : l)
// ClassCastException se s instance of Smiley
System.out.println( ((Car)s.)brand() );
}
Continua…
instanceof
• Permette di determinare il tipo dinamico di una
variabile
x istanceof T è true solo se x ha tipo dinamico T
• Quindi permette di evitare errori in esecuzione
if (x instanceof T)
return (T) x
• Esegue correttamente, perchè x è sicuramente un T
Cast
• Questo, finalmente, è corretto
public static void printBrand(List<Shape> l)
{
for (Shape s : l)
if (s instanceof Car)
System.out.println( ((Car)s.)brand() );
}
Domanda
• E se volessimo disegnare solo le Shapes che
sono Cars?
Risposta
// disegna tutte le Cars della GWin
public void paint(){
for (Shape c:shapes)
if (c instanceof Car) c.draw(g);
}
Scarica

05Subtyping