Interfacce e Polimorfismo - Università Ca' Foscari Veneziapo/Slides/04Interfaces.pdf ·...

Preview:

Citation preview

Interfacce e Polimorfismo

Interfacce

• Dichiarazioni di tipi riferimento che descrivono oggetti in modo astratto

• Specificano solo le firme dei metodi tralasciando tutti gli aspetti di implementazione

Interfacce

interface Poly { public Poly somma (Poly p);

public Poly derivata();

public int grado();

public boolean equals (Poly p);

public double coefficient(int degree);

}

Interfacce

interface Collection<E> { public void add(E element);

public void remove(E element);

public boolean find(E element);

. . .

}

Classi implementano Interfacce

• La keyword implements indica che una classe implementa una interfaccia

class TreeSet<E> implements Collection<E>

{ ... }

class ArrayPoly implements Poly{ ... }

Classi implementano Interfacce

• Una classe che implementa una interfaccia deve implementare tutti i metodi dell’interfaccia

• Tutti I metodi dell’interfaccia devono essere dichiarati public nella classe

• Nulla vieta che la classe dichiari altri metodi oltre quelli dell’interfaccia che implementa

Classi implementano Interfacce• Una interfaccia può essere implementata da più

classi

class ListPoly implements Poly

{ ... }

class ArrayPoly implements Poly { ... }

Classi implementano Interfacce• Una interfaccia può essere implementata da più

classi

class TreeSet<E> implements Collection<E>

{ ... }

class HashMap<E> implements Collection<E>{ ... }

Conversioni di tipo

• Una interfaccia “rappresenta” tutte le classi che la implementano

• Il tipo interfaccia si può associare a tutti gli oggetti delle classi che la implementano

Continua…

Conversioni di tipo

• Possiamo assegnare un riferimento di tipo classe ad una variabile di tipo interfaccia purchè la classe implementi l’interfaccia

Collection<Double> c = new TreeSet<Double>();

Collection<Double> d = new HashMap<Double>();

Poly p = new ArrayPoly();

Poly q = new ListPoly();

Continua…

Conversioni di tipo

• La conversione è lecita solo in determinate situazioni

• Problema: TreeSet<Double>() non implementa Poly

Poly x = new TreeSet<Double>(); // ERRORE

Conversioni di Tipo

• Sottotipo (<:)– La relazione che permette di decidere quando è lecito

convertire un tipo riferimento in un altro – Per il momento diciamo

• Principio di sostituibilità– Ovunque ci si aspetti un riferimento di un tipo è lecito

utilizzare un rifetimento di un sottotipo

Continua…

S <: T sse ● S è una classe, T è una interfaccia ● S implementa T.

Conversioni di Tipo

• Assumiamo S <: T

• Regola di assegnamento– Un riferimento di tipo S si puo sempre assegnare ad una variabile

di tipo T – Un riferimento di tipo classe può sempre essere assegnato ad

una variabile di tipo interfaccia che la classe implementa

• Regola di passaggio di parametri– Un riferimento di tipo S si puo sempre passare per un parametro

di tipo T– Un riferimento di tipo classe può sempre essere passato per un

parametro di tipo interfaccia che la classe implementa

Conversioni di Tipo

• Le conversioni abilitate dalla regola

C <: I sse C implementa I

sono “corrette” – garantiscono che le proprietà che valgono

utilizzando valori di tipo I valgono usando valori di tipo C

• Dimostrazione intuitiva: – Se C implementa I , C deve definire tutti i metodi

dichiarati da I (e dichiararli public)– Quindi tutti le invocazioni di metodo ammesse da I

trovano effettivamente un metodo definito in C Continua…

Polimorfismo – dynamic dispatch

• Le regole di conversione di tipo garantiscono che una variabile di tipo interfaccia ha sempre come valore un riferimento di una classe che implementa l’interfaccia

• Se una interfaccia è implementata da più classi, il tipo dei valori legati alla variabile può cambiare

Continua…

Poly p;p = new ListPoly(...);p = new ArrayPoly(...);

Polimorfismo – dynamic dispatch

• Possiamo invocare ognuno dei metodi dell’interfaccia:

• Quale metodo invoca?

int g = p.grado();

Polimorfismo – dynamic dispatch

• Se p riferisce un ListPoly, invoca il metodo ListPoly.grado()

• Se p riferisce un ArrayPoly, invoca il metodo ArrayPoly.grado();

• Polimorfismo (molte forme): – il metodo invocato dipende dipende dal tipo del

riferimento legato alla variabile

Continua…

int g = p.grado();

• 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: Quadrato e Cerchio

Esempio

Forme grafiche

class Quadrato{ . . . public void draw(Graphics2D g) { // Istruzioni per il disegno . . . }} class Cerchio

{ . . . public void draw(Graphics2D g) { // Istruzioni per il disegno . . . }}

• Un contenitore di Quadrati e Cerchi

GWin

/** Una finestra che contiene Quadrati e Cerchi */ 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

• Ridefiniamo le classi Quadrato e Cerchio in modo che implementino Shape

• Memorizziamo gli oggetti della componente in una ArrayList<Shape>

interface Shape { void draw(Graphics2D g); }

Shapes

class Quadrato implements Shape

{ . . . public void draw(Graphics2D g) { // Istruzioni per il disegno . . . }}

class Cerchio implements Shape

{ . . . public void draw(Graphics2D g) { // Istruzioni per il disegno . . . }}

Mantiene una ArrayList<Shape>

GWin

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

GWin

Cerchio

Shape

Quadrato

Ancora Interfacce

Esempio

• Definiamo una classe DataSet che permette di condurre alcune semplici analisi su un insieme di conti bancari

– calcolo della media degli importi del saldo– calcolo del conto con il valore massimo tra i saldi

Continua…

DataSetpublic class DataSet { public void add(BankAccount x) { if (x == null) return; sum = sum + x.getBalance(); if (count == 0 || maximum.getBalance() < x.getBalance()) maximum = x; count++; } public BankAccount getMaximum() { return maximum; } public double average() {return (count>0)? sum /count : Double.NaN; } private double sum; private BankAccount maximum; private int count; }

Esempio – Data Analysis

• Ora ripetiamo l’esempio per definire una versione della classe DataSet che permette di condurre alcune semplici analisi su un insieme di corsi universitari:

– calcolo della media del numero di studenti per corso– calcolo del corso con massimo numero di studenti

class Corso{ // . . . public double iscritti() { // ... }}

DataSet – versione per Corsopublic class DataSet { public void add(Corso x) { if (x == null) return; sum = sum + x.iscritti(); if (count == 0 || maximum.iscritti() < x.iscritti()) maximum = x; count++; }

public Corso getMaximum() { return maximum; } public double average() {return (count>0)? sum/count : Double.NaN; } private double sum; private Corso maximum; private int count; }

Interfacce riuso di codice

• Il meccanismo di analisi dei dati è sempre lo stesso; – si basa sull’estrazione di una misura – la differenza è solo nell’implementazione del metodo che

fornisce la misura

• Possiamo uniformare: – è sufficiente stabilire una modalità uniforme per estrarre la

misura.

interface Measurable { double getMeasure(); }

Measurable BankAccounts

public class BankAccount implements Measurable { public double getMeasure() { return getBalance(); } // ... }

Continua…

Measurable Corsi

class Corso implements Measurable { public double getMeasure() { return iscritti(); } // . . . }

DataSet – versione generica

public class DataSet{ public void add(Measurable x) {

if (x == null) return; sum = sum + x.getMeasure(); if (count == 0 || maximum.getMeasure() < x.getMeasure()) maximum = x; count++; } public Measurable getMaximum() { return maximum; } public double average() { return sum/count; } private double sum; private Measurable maximum; private int count; }

DataSet Diagramma UML

dipende

implementa

Corso

Interfacce multiple

• Una classe può implementare più di una interfaccia

• In quel caso deve definire, public, tutti i metodi delle interfacce che implementa

• E’ automaticamente sottotipo di ciascuna delle interfacce che implementa

Measurable Shapes

class Quadrato implements Shape, Measurable

{ . . . public void draw(Graphics2D g) { ... } public double getMeasure() { ... } }

class Cerchio implements Shape, Measurable

{ . . . public void draw(Graphics2D g){ ... } public void getMeasure() { ... }}

Measurable Shapes

• Ora possiamo passare Quadrati e Cerchi sia all’interno della classe Gwin per essere disegnati, sia all’interno della classe DataSet per essere misurati

Conversioni di tipo

• Una interfaccia “rappresenta” tutte le classi che la implementano

• Più in generale: un supertipo rappresenta tutti i suoi sottotipi

Continua…

Conversioni di tipo

• 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…

Ancora Shapes

class Smiley implements Shape

{ . . .public void draw(Graphics2D g){ . . . }public String mood() {. . . }

}

class Car implements Shape

{ . . .public void draw(Graphics2D g){ . . . }public String brand() {. . . }

}

Sottotipi 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

System.out.println(s.brand());

Continua…

Sottotipi 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

• 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…

((Car)s).brand()

Tipo statico e Dinamico

• Tipo statico e tipo dinamico di una variabile– tipo statico: quello dichiarato – tipo dinamico: il tipo del riferimento assegnato alla

variabile

• Il tipo dinamico può cambiare durante l’esecuzione, ma le regole garantiscono che

– Il tipo dinamico di una variabile è sempre un sottotipo del tipo statico della variabile

Continua…

Cast

• (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

• OK: Car sottotipo di Shape

• Compila correttamente – il tipo dichiarato di s è Shape – Car e Shape sono compatibili

• Esegue correttamente – s è un Car (il tipo dinamico di s è Car)

Shape s = new Car();

Continua…

Car c = (Car) s

Cast

• OK: Car sottotipo di Shape

• Compila correttamente – il tipo dichiarato di s è Shape – Smiley e Shape sono compatibili

• Errore a run time – s non è uno Smiley

Shape s = new Car();

Continua…

Smiley c = (Smiley) s

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

• Quindi permette di evitare errori in esecuzione

• Esegue correttamente, perchè x è sicuramente un T

x istanceof T è true solo se x ha tipo dinamico T

if (x instanceof T) return (T) x

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

• Come 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); }

Polinomi

• Polinomi (http://www.dais.unive.it/~po/Labs/interfaces.html)

interface Poly { public Poly somma (Poly p);

public Poly derivata();

public int grado();

public boolean equals (Object q)

public boolean equals (Poly p);

public double coefficient(int degree);

}

Polinomi – equals()

• Notiamo le due versioni – entrambi sono necessarie – Dimostrano un aspetto interessante dell’interazione

tra il meccanismi di polimorfismo (dynamic dispatch) e di risoluzione dell’overloading

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)”; } }

Polinomi – equals()

• Consideriamo:

• Data la dichiarazione q:Object la chiamata p.equals(q)viene associata alla firma Poly.equals(Object)

• eDinamincamente, poi, viene dispacciata al corpo del metodo ArrayPoly.equals(Object)

Poly p = new ArrayPoly(); Object q = new ListPoly(); p.equals(q);

Interfacce standard – Listeners

• Le librerie Java forniscono molte interfacce standard, che possono essere utilizzare nella programmazione

• Un insieme interessante di queste interfacce è legata al framework Swing, ed utilizzata per la programmazione ad eventi, tipica delle applicazioni con interfaccia grafica

Eventi, Sorgenti, e Listeners• Una interfaccia utente deve gestire una

moltitudine di eventi– eventi da tastiera, del mouse, click su pulsanti, …

• Opportuno poter discriminare diversi eventi– componenti specifiche devono poter essere

programmate senza preoccuparsi che di eventi specifici

• Necessario avere meccanismi per gestire eventi in modo selettivo

– Un meccanismo per dichiarare quali eventi si intendono gestire

Continua…

• Listener: – Oggetto che viene notificato dell’occorrere di un

evento – I suoi metodi descrivono le azioni da eseguire in

risposta ad un evento

• Sorgente di eventi: – Oggetto che origina eventi, o li emette, – Notifica l’evento ai propri listeners

• Vari tipi di sorgenti– Componenti visuali: JButton, JTextField, ...– Timer, …

Eventi, Sorgenti, e Listeners

Continua…

• Ogni sorgente genera un insieme ben definito di eventi

– determinato dal tipo della sorgente

• Eventi di basso livello– mouse-down/up/move, key-press/release,

component-resize/move, …

• Eventi “semantici”– ActionEvent: click su un JButton, doppio-

click su un JListItem, tick di un Timer– TextEvent: JTextField modificato da uno

o più mouse/key events

Eventi, Sorgenti, e Listeners

Continua…

• Gestione mirata degli eventi– Una applicazione può decidere quali eventi gestire tra

quelli generati da ciascuna sorgente

• Registra su ciascuna componente solo i listeners degli eventi che intende gestire

Eventi, Sorgenti, e Listeners

Continua…

Eventi, Sorgenti, e Listeners

• Esempio: utilizziamo una componente JButton per realizzare un pulsante: associamo a ciascun pulsante un ActionListener

• Ricordiamo:

• Dobbiamo fornire una classe il cui metodo actionPerformed contiene le istruzioni da eseguire quando il pulsante viene premuto

public interface ActionListener { void actionPerformed(ActionEvent event); }

Continua…

Eventi, Sorgenti, e Listeners

• event contiene informazioni sull’evento (ad esempio l’istante in cui è avvenuto)

• Associamo il listener al pulsante, registrandolo sul pulsante:

• addActionListener(): un metodo di JButton

ActionListener listener = new ClickListener(); button.addActionListener(listener);

void addActionListener(ActionListener l);

File ClickListener.java

01: import java.awt.event.ActionEvent;02: import java.awt.event.ActionListener;03: 04: /**05: Un action listener che stampa un messaggio06: */07: public class ClickListener implements ActionListener08: {09: public void actionPerformed(ActionEvent event)10: {11: System.out.println("I was clicked.");12: } 13: }

File ClickApp.javaimport java.awt.event.ActionListener;import javax.swing.JButton;import javax.swing.JFrame; public class ClickApp{ public static void main(String[] args) { JFrame frame = new JFrame(); JButton button = new JButton("Click me!"); frame.add(button);

button.addActionListener(new ClickListener());frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } private static final int FRAME_WIDTH = 100; private static final int FRAME_HEIGHT = 60;}

File ClickListener.java

Output:

Eventi del mouse

• mousePressed, mouseReleased: invocati quando un pulsante del mouse viene premuto o rilasciato

• mouseClicked: invocato quando un pulsante del mouse viene cliccato

• mouseEntered, mouseExited: il mouse è entrato o è uscito dall’area della componente

Eventi del mouse• Catturati da MouseListeners

Continua…

public interface MouseListener { // un metodo per ciascun mouse event su una componente

void mousePressed(MouseEvent event); void mouseReleased(MouseEvent event); void mouseClicked(MouseEvent event); void mouseEntered(MouseEvent event); void mouseExited(MouseEvent event); }

Eventi del mouse

• Se vogliamo che una componente reagisca ad eventi del mouse dobbiamo registrare un MouseListener sulla componente:

Continua…

public class MyMouseListener implements MouseListener { // Implementa i cinque metodi} MouseListener listener = new MyMouseListener(); component.addMouseListener(listener);

Mouse Events

class MousePressListener implements MouseListenere { public void mousePressed(MouseEvent event) { int x = event.getX(); int y = event.getY(); . . . }

// implementazione degli altri metodi dell’interfaccia}

• Tutti i metodi devono essere implementati; implementazione vuota se inutili

ActionListener

• Descrive oggetti che svolgono il ruolo di gestori di eventi:

– rimane attesa (in ascolto) di un evento, per poi rispondere con una azione al momento in cui l’evento si manifesta

interface ActionListener{ void actionPerformed(ActionEvent event);}

Eventi di un timer

• Eventi notificati ad un ActionListener associato al timer

• Il gestore viene attivato ad ogni tick del timer

– actionPerformed() invocato ad ogni tick– event: contiene informazione sull’evento

public interface ActionListener{ void actionPerformed(ActionEvent event);}

Continua…

Eventi di un timer

• La gestione dell’evento avviene nel metodo actionPerformed()

• Gestioni diverse realizzate da classi diverse che implementano ActionListener

class TimerListener implements ActionListener{ public void actionPerformed(ActionEvent event) { // Eseguito ad ogni tick. }}

Continua…

Eventi di un timer

• Per associare un particolare listener al timer è necessario registrare il listener sul timer

• Ora possiamo far partire il timer

TimerListener listener = new TimerListener();Timer t = new Timer(interval, listener);

t.start(); // Esegue in un thread separato

tra due tick

Esempio: countdown

• Un timer che esegue il countdown

CountDownListener

public class CountDownApp{ public static void main(String[] args) { CountDownListener listener = new CountDownListener(10); // Millisecondi tra due tick final int DELAY = 1000; Timer t = new Timer(DELAY, listener); t.start();

JOptionPane.showMessageDialog(null, "Quit?"); System.exit(0); }}

CountDownListener

• Inizializza un contatore al valore passato nel costruttore

• Ad ogni invocazione del metodo actionPerformed()

– controlla il valore del contatore e dà in output il messaggio corrispondente

– decrementa il contatore

CountDownListener

class CountDownListener implements ActionListener{ public CountDownListener(int initialCount) {

count = initialCount; }

public void actionPerformed(ActionEvent event) {

if (count >= 0) System.out.println(count);if (count == 0) System.out.println("Liftoff!");count--;

} private int count;}