19
In questa lezione le seguenti slide non stati trattati alla lavagna: queue, collections, convenzione lettere tipi parametrici, notazione generics. Lezione scorsa • Collections • Interfaccia Set • Implementazioni HashSet, TreeSet • Extends e super • Comparable e Comparator • Erasure e Cast Scopo del corso é: come si spezza in classi una applicazione e come fare le firme dei metodi, più che la loro implementazione (l'implementazione poi è guidata dalle firme dei metodi; in alcune casi è anche importante l'implementazione dei metodi, come nel caso degli Iteratori). La lezione scorsa abbiamo visto ancora cose sulle collection, in particolare sugli insiemi che hanno la loro interfaccia dedicata che si chiama Set (in generale ogni collection ha una interfaccia specifica che dice quali operazioni si possono fare a cui segue più tipologie di implementazioni). Abbiamo visto che tale interfaccia ha più implementazioni diverse che differiscono per la complessità con cui vengono implementate. Ad esempio in HashSet abbiamo che ogni operazione ha un tempo costante di esecuzione, mentre nel TreeSet ogni operazione ha un tempo "logaritmico" di esecuzione. Comunque quello che ci interessa sapere è che Java mette a disposizione per ogni interfaccia delle implementazioni diverse. Abbiamo visto che il comparator è necessario al TreeSet per poter funzionare. Abbiamo visto anche che il compilatore controlla i tipi e che dopo a runtime i tipi generici spariscono, però il compilatore ci assicura che a runtime non ci sarà nessun problema (cioè nessun eccezione ClassCast viene sollevata). Quindi dobbiamo ricordarci che per i tipi parametrici (Generics) il controllo viene fatto dal compilatore (e questa è particolarità del linguaggio Java, in C++ il discorso è diverso). Queue: L’interfaccia Queue una coda ovvero una struttura dove si inseriscono elementi (ingresso in coda) e solo un elemento (la testa della coda) è disponibile per l’accesso o per l’estrazione (uscita dalla coda). La politica di selezione dell’elemento in testa è tipicamente FIFO ma sono possibili anche code con “scavalcamento” basato su qualche criterio di ordinamento o priorità. Esistono tre operazioni base su una Queue: – inserimento di un elemento in coda – accesso all’elemento in testa (senza toglierlo dalla coda) – estrazione dell’elemento in testa (eliminandolo dalla coda) Di ognuna esistono due versioni a seconda del comportamento quando l’operazione non è possibile: una lancia un’eccezione, l’altra ritorna false o null Lanciano eccezione: - boolean add(E e): Aggiunta Non lanciano eccezione: - boolean offer(E e): Aggiunta

Lezione20-21 PO Mod1 Roncato

Embed Size (px)

DESCRIPTION

Lezione20-21 PO Mod1 Roncato

Citation preview

Page 1: Lezione20-21 PO Mod1 Roncato

In questa lezione le seguenti slide non stati trattati alla lavagna: queue, collections, convenzione lettere tipi parametrici, notazione generics.

Lezione scorsa• Collections• Interfaccia Set• Implementazioni HashSet, TreeSet• Extends e super• Comparable e Comparator• Erasure e Cast

Scopo del corso é: come si spezza in classi una applicazione e come fare le firme dei metodi, più che la loro implementazione (l'implementazione poi è guidata dalle firme dei metodi; in alcune casi è anche importante l'implementazione dei metodi, come nel caso degli Iteratori).

La lezione scorsa abbiamo visto ancora cose sulle collection, in particolare sugli insiemi che hanno la loro interfaccia dedicata che si chiama Set (in generale ogni collection ha una interfaccia specifica che dice quali operazioni si possono fare a cui segue più tipologie di implementazioni). Abbiamo visto che tale interfaccia ha più implementazioni diverse che differiscono per la complessità con cui vengono implementate. Ad esempio in HashSet abbiamo che ogni operazione ha un tempo costante di esecuzione, mentre nel TreeSet ogni operazione ha un tempo "logaritmico" di esecuzione. Comunque quello che ci interessa sapere è che Java mette a disposizione per ogni interfaccia delle implementazioni diverse.

Abbiamo visto che il comparator è necessario al TreeSet per poter funzionare.

Abbiamo visto anche che il compilatore controlla i tipi e che dopo a runtime i tipi generici spariscono, però il compilatore ci assicura che a runtime non ci sarà nessun problema (cioè nessun eccezione ClassCast viene sollevata). Quindi dobbiamo ricordarci che per i tipi parametrici (Generics) il controllo viene fatto dal compilatore (e questa è particolarità del linguaggio Java, in C++ il discorso è diverso).

Queue: L’interfaccia Queue una coda ovvero una struttura dove si inseriscono elementi (ingresso in coda) e solo un elemento (la testa della coda) è disponibile per l’accesso o per l’estrazione (uscita dalla coda). La politica di selezione dell’elemento in testa è tipicamente FIFO ma sono possibili anche code con “scavalcamento” basato su qualche criterio di ordinamento o priorità.Esistono tre operazioni base su una Queue: – inserimento di un elemento in coda– accesso all’elemento in testa (senza toglierlo dalla coda)– estrazione dell’elemento in testa (eliminandolo dalla coda)Di ognuna esistono due versioni a seconda del comportamento quando l’operazione non è possibile: una lancia un’eccezione, l’altra ritorna false o null

Lanciano eccezione:- boolean add(E e): Aggiunta elemento - E element(): Accesso elemento in testa- E remove(): Estrazione elemento in testa

Non lanciano eccezione:- boolean offer(E e): Aggiunta elemento- E peek(): Accesso elemento in testa- E poll(): Estrazione elemento in testa

Eredita metodi dalla interfaccia Collection. Non può essere usata per generare degli oggetti, ma impone, alle classi che la utilizzano di implementare i seguenti metodi:

- public boolean add(T e); aggiunge un elemento nella coda indicato dal parametro T; se l'elemento inserito viola le restrizioni di capacità legate alla coda, allora verrà lanciata l'eccezione IllegalStateException- public boolean offer(T e); inserisce in una coda l’elemento indicato dal parametro e solo se l’inserimento è attuabile. Tale metodo differisce dall’equivalente metodo add in quanto, se l’elemento non è stato inserito (per esempio perché l’implementazione della coda ha posto dei vincoli sulla capacità massima di elementi inseribili), non lancerà alcuna eccezione ritornando semplicemente il valore false .

Page 2: Lezione20-21 PO Mod1 Roncato

- public T remove(); ritorna l’elemento posto in testa a una coda, rimuovendolo; se la coda è vuota il metodo lancerà un’eccezione di tipo NoSuchElementException- public T poll();ritorna l’elemento posto in testa a una coda, rimuovendolo; se la coda è vuota, ritornerà il valore null- public T element();ritorna l’elemento posto in testa a una coda senza rimuoverlo. Se la coda è vuota il metodo lancerà un’eccezione di tipo NoSuchElementException- public T peek(); ritorna l’elemento posto in testa a una coda senza rimuoverlo. se la coda è vuota, ritornerà il valore null

L'implementazione della interfaccia Queue avviene tramite:• LinkedList: già vista• PriorityQueue: log(n) per operazioni generiche mentre O(1) for peek, element.

Map: E' l'ultima interfaccia che vediamo. L’interfaccia Map rappresenta un insieme di elementi (o valori) ciascuno dei quali è associato a una chiave che lo riferisce. Ricordiamo che una mappa non può avere chiavi duplicate e che ogni chiave può riferire solamente un valore. Ogni coppia chiave/valore è definita anche entry.

Una mappa è una interfaccia che contrariamente a tutte le collezioni viste finora ha due tipi generici. La Lista la potevano fare di un solo tipo, la mappa invece mi dice "dato un qualcosa, tornami l'oggetto vi è associato". Quindi la mappa la possiamo vedere come una funzione dove dato un "qualcosa" K, ci associo un "oggetto" V.

Vediamo innanzitutto come si usa e poi andiamo a vedere in dettaglio le caratteristiche. Immaginiamo di dover far una struttura dati che memorizza il nome dello studente a cui corrisponde una certa matricola. Tale struttura dati è fatta così: Map <Integer, String>, dove Integer sarà il numero di matricola e String il nome dello studente. Per la Map possiamo avere varie implementazioni e scegliamo l'HashMap (la preferita del prof.), e all'inizio Studente è una mappa vuota.

I dati dentro ad una mappa (che è in questo caso una tabella Hash) si inseriscono con il metodo Put; tramite il Put possiamo vedere Map come quella funzione che associa i valori K ai valori V: K è il dominio della funzione (ed è anche chiamato chiave) e il V è il codominio (ed è anche chiamato value).

Osservazione. Map può essere visto come una funzione:• f: K->V , K Dominio, V codominio, ad ogni elemento del dominio è associato uno ed uno solo elemento del codominio.• Se K finito,(|K|=n), allora possiamo rappresentare la funziona con una mappa.1) Map<K,V> map = new ...2) Per ogni k in K, map.put(k,f(k));3) f(k) == map.get(k) per ogni k.

Quindi, data una Map<K,V> map; Possiamo definire la funzione f: K->V associata alla mappa map nel seguente modo: f(k) = v in V tale che v=map.get(k);La funzione f rimane tale finché la mappa non viene modificata (es. tramite put o remove);

Page 3: Lezione20-21 PO Mod1 Roncato

Per tutte le chiavi k in cui non è definita la mappa, la funzione vale null.

Le mappe sono qualcosa di più delle funzioni, perchè le posso modificare anche a runtime perchè inserisco dati e le cose che non ho definito ritornano null.

Torniamo alla struttura che vogliamo definire:Map<Integer, String> Studente = new HashMap<>(); //creo tabella Hash vuotaStudente.Put(2222, “Paolo Rossi”); //il numero è il valore K e la stringa è il valore VStudente.Put(5555, “Pinco Pallo”);Studente.Put(7777 , “Pluto Nash”);

Di norma come regola, nei linguaggi ad oggetti, come tecnica di programmazione, è meglio usare l'interfaccia al posto del tipo vero, questo perchè se un domani mi accorgo che il codice scritto funziona meglio su un TreeMap (è una cosa più complessa da usare) piuttosto che su un HashMap, è più semplice fare i cambiamenti. Addirittura se avessi un sottotipo, cioè una classe che estende, è sempre meglio usare la classe base perchè chiedo meno vincoli in caso di cambiamenti futuri.

Se voglio il nome di un certo studente uso il metodo get, che accede in tempo costante alla struttura dati.

String nome = Studente.get(2222); //la chiave torna il valore associatoString s = Studente.get(7778); //get torna null se non trova il riferimento cercato, quindi a

//quella chiave è associato null; quindi s prende null

Posso sovrascrivere le varie chiavi:Studente.put(55, “Carol”);Studente.put(55, “Ilaria”); //sovrascrive la riga precedente e quindi il valore

//associato alla medesima chiave

I metodi principali sono put (imposto il valore) e get. Ci sono poi tanti altri metodi, di cui tre particolari e complessi. Iniziano da quelli più semplici.

Altri metodi: - int Size(); torna il numero di chiavi che sono state definite - boolean isEmpty(); Nelle mappe abbiamo due tipi di contains, nelle altre collezioni solo uno.- boolean contains Key (Object K); controlla se c’e’ la chiave nella mappa- boolean contains Value (Object K); controlla se c’e’ il valore nella mappa- V remove (Object K); data la chiave rimuove l'oggetto e torna il value associato, altrimenti torna null- putAll (Map<? extends K, ? extends V> m); aggiunge alla mappa creata, un'altra mappa passata come argomento e possiamo passargli qualsiasi mappa che abbia dei tipi migliori di quelli della mappa originaria, cioè dei sottotipi per la chiave e value originarie- void clear(); pulisce la mappa, è come se la mappa fosse vergine, cioè la resetta; questo metodo può essere utile per ottimizzare la memoria, soprattutto perchè le mappe fatte con le tabelle Hash occupano molta memoria

Mancano tre metodi che sono più complessi; i primi due permettono di esaminare il contenuto della mappa. La mappa non parte di quel gruppo che estendono le collection e collection implementa Iterable, quindi le mappe non hanno la possibilità di essere iterate. Quindi la mappa non si può scorrere, ma posso usare i seguenti due metodi per scorrere o sulle chiavi o sui valori:- set<K> keySet(); scorre sulle chiavi della mappa- Collection<V> values(); scorre sui valori della mappa

Esempio per scorrere la mappa sui valori K e V:1) for (String V: Studente.values())

2) for (Integer K : Studente.keySet())

Page 4: Lezione20-21 PO Mod1 Roncato

MAPil ciclo 1) scorre <- K= Integer String =V -> qua il ciclo for del 2) scorre

sulla colonna Integer 123 Pippo sulla colonna String456 Pluto876 Paperino34143 Pippo

Notiamo che il keySet() è un insieme, quindi non posso avere duplicati (perchè se provo ad usare dei duplicati, in realtà sovrascrivo la medesima chiave con un nuovo valore, e di conseguenza o una unica copia). Quindi le chiavi sono un insieme, i valori invece sono un collezione (e qua posso avere due valori uguali, su chiavi diverse).

Il terzo metodo complicato è il seguente:- Set<Map.Entry<K,V>> entrySet(); questo metodo permettere di vedere le chiavi e i valori insieme, come coppie; il metodo ritorna un insieme i cui elementi sono di tipo entry, che è una classe definita all'interfaccia Map (quindi è una classe interna o sottointerfaccia); entry è una coppia (chiave, valore) e la indico con e. Essendo un insieme possiamo iterarlo (perchè essendo un insieme è una collection).

Vediamo come è definita questa sottointerfaccia (che ha due tipi come la mappa) dentro la interfaccia Map:Interface Map<K, V>{ // definizione della sottointerfaccia entry; ha tre metodi

Static interface Entry<K,V>{ K.getKey(); V.getValue(); V.setValue(value); // consente di modificare, con un nuovo valore, il valore dell’attuale entry

}}

A che serve? se facciamo un numero elevato di operazioni di modifica della mappa, invece di fare tanti put e tanti get che potrebbero essere onerosi, usare una struttura come questa può essere più veloce. Ad esempio supponiamo di avere una mappa che sia da intero ad intero e voglia trasformare tutto al quadrato, usando la combinazione di put e get, dovrei accedere a tutti gli elementi del dominio, farmi ritornare i relativi valore del codominio, trasformali al quadrato e reinserire il nuovo valore, se tale mappa è implementata con Treeset la cosa risulta molto onerosa perchè ogni operazione richiede log(n). Con Interface Entry invece tutto è più veloce e maneggevole. Lo static davanti ad interface vuol dire che la classe con cui poi verrà implementata non ha bisogno di accedere al this della classe che sta sopra (che lo ospita). Se non ci fosse lo static la differenza sta nell'uso della memoria: se c'e' static non ho bisogno in Entry di avere un puntatore alla classe Map che la ospita.

Es.: voglio mettere tutti i nomi degli studenti in maiuscolo, e poichè entrySet mi torna un insieme, allora posso iterarlo. Da notare che e ora rappresenta delle coppie.

for (Map.Entry<Integer, String> e : Studente.entrysSet()) e.setValue(e.setValue().ToUpper()); // prendo il valore vecchio, lo faccio maiuscolo e sostituisco questo valore nuovo a quello vecchio; e lo faccio con un for unico in temp O(n)

In alternativa potevo ad esempio scorrere sulle chiavi :for (Integer i : Studenti.keySet())

studente.put(i, studente.get(i).ToUpper()); // richiede O(n logn) se uso un Treeset ed è molto meno efficiente

Ricapitolando, di seguito vi è la interaccia nel suo complesso:

public interface Map<K,V> {int size();boolean isEmpty();boolean containsKey(Object key);boolean containsValue(Object value);V get(Object key);V put(K key, V value);V remove(Object key) ;

Page 5: Lezione20-21 PO Mod1 Roncato

void putAll(Map<? extends K, ? extends V> m);void clear();Set<K> keySet();Collection<V> values();static interface Entry<K,V>{

public K getKey();public V getValue();public V setValue(V value);

}Set<Map.Entry<K, V>> entrySet();

}

Questa interfaccia ha i seguenti metodi

V put(K key, V value): inserisce in una mappa il valore fornito dal parametro value associandogli la relativa chiave fornita dal parametro key. Se la mappa contiene già un valore per la chiave key, allora lo stesso sarà sostituito dal nuovo valore value e il vecchio valore sarà ritornato dal metodo.

void putAll(Map<? extends K,? extends V> m): inserisce in una mappa tutte le coppie chiave/valore della mappa fornita dal parametro m.

V get(Object key): ritorna il valore associato dalla chiave indicata dal parametro key. Se la chiave non è presente sarà ritornato il valore null.

V remove(Object key): rimuove l’associazione della chiave di cui il parametro key ritornando il relativo valore oppure il valore null se la chiave non è presente.

void clear(): rimuove da una mappa tutte le coppie chiave/valore. int size(): ritorna la quantità di coppie chiave/valore presenti in una mappa. boolean containsKey(Object key): verifica se una mappa contiene la chiave indicata dal parametro key. boolean containsValue(Object value): verifica se una mappa contiene il valore indicato dal parametro value. boolean isEmpty(): verifica se una mappa è vuota. Collection<V> values(): ritorna una collezione di tipo Collection di tutti i valori di una mappa. Qualunque

cambiamento effettuato sulla vista ritornata influenzerà la relativa mappa e viceversa. Set<Map.Entry<K,V>> entrySet(): ritorna una collezione di tipo Set di tutte le coppie Set<K> keySet() ritorna una collezione di tipo Set di tutte le chiavi di una mappa. Qualunque cambiamento

effettuato sulla vista ritornata influenzerà la relativa mappa e viceversa. Il tipo Entry<K, V> è un’interfaccia definita all’interno dell’interfaccia Map che astrae il concetto di coppia

chiave/valore. Dispone dei seguenti metodi: getKey per ottenere la chiave dell’entry, getValue per ottenere il valore dell’entry e setValue che consente di modificare, con un nuovo valore, il valore dell’attuale entry.

Map può essere implementato con:• HashMap: tabella hash O(1)• Hashtable: (gestisce concorrenza)• IdentityHashMap: usa == invece di equals• LinkedHashMap: HashMap+LinkedList• TreeMap: log(n)• WeakHashMap: usa == e non “trattiene” i valori quando le chiavi non sono più usate

Collections• java.util.Collection interfaccia• java.util.Collections classe di utilità:

– sort– reverse– shuffle– sort– binarySearch– <T> void copy(List<? super T> dest, List<? extends T> src)

Page 6: Lezione20-21 PO Mod1 Roncato

Convenzione lettere tipi paramentrici• E: Tipo degli elementi in una collezione• x: Tipo delle chiavi in una mappa• V: Tipo dei valori in una mappa• T: Tipo generico• S,U: Altri tipi generici

Notazione generics• <T> <?> <K,V> <T,S> <?,?> etc.• <T extends x> <? extends x>• <T super x> <? super x>• <T extends y&z> <? extends y&z>• Dove: x può essere interfaccia, classe o un'altra variabile di tipo• y può essere interfaccia o una classe• z può essere interfaccia

Mutable - immutable: in generale gli oggetti sono modificabili o immodificabili. L'oggetto Date ad esempio appena creato si può modificare. Anche le collecion e le mappe (e tutti i loro sottogruppi) sono modificali.• Le collezioni viste fin'ora sono mutabili ovvero ne possiamo modificare il contenuto dopo averle create:es. la lista la creo vuota o poi ci aggiungo gli elementi:List<Integer> l = new LinkedList<>();l.add(1);//modifical.add(2);//modifical.remove(3);//modificaint i = l.size()//non modificaModificare vuol dire cambiare i valori della variabili interne. Immutabili vuol dire che una volta che l'oggetto è creato, non può essere più modficato. Per le collezioni e in generale per gli oggetti è possibile definirli in modo che siano immutabili ovvero non modificabili dopo la creazione.• Java definisce già oggetti immutabili: String, Integer, Boolean, etc. cioè gli oggetti che chiamiamo Wrapper. In JAva non si cambia il valore delle stringhe.• Altri li possiamo definire noi.• A cosa può servire un oggetto immutabile?1) Semplifica la gestione concorrente (+Thread)2) Evita la modifica non autorizzata delle classiPer approndimento, vedi post: http://cosenonjaviste.it/le-classi-immutabili-in-java/

Esempio: StringGli oggetti String dopo la creazione non si cambiano.• Metodo toLower della classe String• Firma: public String toLower();• In pratica invece di modificare l'oggetto this cambiandone il contentuto, si lascia inalterato e si ritorna una nuova stringa il cui contentuo è il contuto di this trasformato in minuscolo.• Es:String l = "Miao".toLower();l.equals("miao")==true;

Vediamo ad esempio questo codice da un punto di vista graficoString s = "a"+nome;s = s + "c";// questa sarà una nuova stringa e di conseguenza il vecchio puntatore è cambiato con un nuovo puntatore ad un oggetto che sarà sempre di tipo String che però contiene una stringa diversa dalla prima; il "+" quindi crea qualcosa di diverso

Page 7: Lezione20-21 PO Mod1 Roncato

Nell'esempio sopra la memoria dell'oggetto (la variabile a) viene riusata, gli immutable invece sono usa e getta (nell'esempio la stringa creata prima, viene buttata via dopo l'uso del "+"). Tutti i metodi che cambiano una stringa, in realtà, creano una nuova stringa con le varie modifiche e quella vecchia viene buttata via.Stessa cosa vale per gli Integer.

Svantaggi oggetti immutabii:- usano un po' troppa memoria perchè per ogni operazione si crea un nuovo oggetto

Se vogliamo usare le stringhe senza che ogni volta venga creata una nuova copia ci sono delle classi apposite in java (cioè la versione "mutabile" di String) che sono:- String Builder -> si usa con un unico thread (no concorrenza)- String Buffer -> si usa in presenza di più Thread che accedono allo stesso oggetto

Le Collection sono mutabile ed esiste per tutte le collection una versione immutabile delle stesse.Ora se ho uno spreco di memoria con un oggetto immutabile, perchè usare lo stesso concetto anche sulle collection che sono una collezione di oggetti che poi dopo il loro uso devo buttare via? Le cose immutabile creano molti meno effetti indesiderati ed errori ed evita che qualcuno dal di fuori vada a modifica l’oggetto su cui lavoriamo.Ad esempio stiamo lavorando sulla class Person, dove dichiator le variabili di tipo private e non metto alcun tipo di metodo set, così nessuno dal di fuori della classe può modificare le variabili

Class Persona{private String nome;private Date nascita; //dal di fuori può essere modificata?

String get.Nome();Date get.Nascita();...

}

Se prendo "nome" essendo di tipo String è immutabile e se vado a cambiare il nome, in realtà creo una copia modificata dell'oggetto originale e quindi un nuovo oggetto, e l'oggetto originale rimane invariato.

Ad esempio con il seguente codice il metodo toUpper() si crea una copia temporanea del nome, fa le modifiche e poi il tutto va in un nuovo oggetto, perchè appunto non c'e' il metodo "set" .

UsoPersona{p.getNome().toUpper();p.get.Nascita.add(....):...

}

Ma osserviamo che se invece prendiamo la variabile nascita, essa è di tipo Date che è mutatile e quindi dal di fuori anche senza set, potrebbe essere modificata a mia insaputa.

Vantaggi uso oggetti immutabili - l'uso di oggetti immutabili garantisce che non possano essere fatte modifiche dall'esterno.- semplificano la gestione concorrente in presenza di più Thread (se l'oggetto è immutabile non ci sono problemi di stato non consistente dell'oggetto stesso)

I mutabili sfruttano meglio la memoria e vanno in bene in contesti single thread (non ci sono modifiche contemporanee)Gli immutabili sfruttano più memoria e sono buoni in contesti concorrenti o quando ci sono problemi di consistenza dei dati.

Page 8: Lezione20-21 PO Mod1 Roncato

Come facciamo a definire una classe immutabile:• Senza metodi che la modificano (solo get)• Variabili d'istanza private (in modo che neanche le sottoclassi possano rompere il vincolo) e final in modo che il compilatore controlli che anche la stessa classe non le modifichi accidentalmente• Copiare i parametri mutabili del costruttore• Copiare i riferimenti mutabili ritornati dai metodi• Classe final (strong) o tutti metodi final (weak)

Come si può creare una Collezione immutabile? Ad esempio una lista che sia immutabile? e i suoi metodi? Dovrebbe essere definita a spanne così:

public class Lista<T> implements Iterable<T>{private final T info;private final Lista<T> next;public Lista<T> vuota();public Lista<T> concatenate(Lista<T> t);public T getHead();//attenzioneprivate Lista<T>(){};public int size();public Itertor<T> iterator();//attenzione

...

Vediamo le cose nel particolare e iniziamo dalle dichiarazione delle variabili e dei costruttori:

public class Lista<E> implements Iterable<E>{ //su iterable non dobbiamo implementare il //metodo remove altrimenti andiamo a modificare la lista, ma remove è opzionale

//per le variabili di istanza vi è final il che vuol dire che dopo aver messo i valori a info e next, questi non possono//essere più modificati

private final E info; private final Lista<E> next;

//creiamo due tipi di liste, ma quella vuota per ora la tralasciamoLista<E>() {}; //serve per creare una lista vuota

//questo costruttore crea una lista a partire dal puntatore al prossimo elemento private Lista<E> (E info, Lista<E> next) { this.info = info;this.next = next;

}

.....

Come facciamo ora a dichiarare i metodi che devono anche loro seguire la filosofia "immutabili"? Facciamo ad esempio il metodo concatena, e so che non posso modificare il this. Concatena prende il this, prende la lista che gli passo come argomento e mi torna una nuova lista che è l'unione delle due. Se avessi avuto una lista mutabile, questo metodo "concatena" sarebbe stato il metodo addAll, ma qui siamo in presenza di qualcosa di diverso.Vediamo come è la situazione graficamente (supponiamo siano liste di interi):- da una parte ho il mio this fatto dei info e next, a suo volta next punta ad una info con associato il relativo next, fino all'ultimo elemento che contiene 3 e il cui puntatore è vuoto:

Page 9: Lezione20-21 PO Mod1 Roncato

- dall'altra parte ho la lista l che voglio aggiungere e che passo come argomento

- alla fine devo costruire una lista che chiamo ret che sia "l'assemblaggio" delle due" e so che non posso modificare nessuno dei valori (perchè sono stati dichiarati final); sostanzialmente devo costruirmi una copia:

Quindi devo procedere a fare una copia, ma poichè entrambe le liste sono immutabili posso evitare di fare la copia "più completa" perchè invece di fare due "oggetti nuovi", posso usare l'oggetto l che già c'e', perchè tanto non lo modifico, esso rimane tale e quale e quindi invece di duplicare tutto si fa un qualcosa di questo tipo:

cioè della prima parte faccio una copia, della seconda parte non occorre fare una copia. Alla fine dobbiamo implementare il seguente algoritmo: duplichiamo la prima parte fino al null e invece di creare il null nell'ultimo elemento, al suo posto ci mettiamo la lista l in modo che si concateni.Quindi il codice del metodo sarà qualcosa del tipo:

public Lista<E> Concatena (Lista<? extends E> l){Lista<E> ret {

if (next!=null)return new Lista(info, next.Concatena(l));//chiamata ricorsiva che è fatto sul

//this (cioè il target) e non sull'argomento passatoelse

return new Lista(info, l);}

}

In questo modo le liste che si creano sono immutabili. Ma abbiamo un'altra difficoltà: come creare la copia; per farlo ho bisogno di altri metodi di supporto.

--inizio lezione del pomeriggio--

Altri metodi che potrebbero essere implementati in filosofia "immutabili" che non modificano quindi la lista?- E getFirst(); torna il primo elemento della lista- E get(int i); torna l'i-simo elemento della lista- int Size(); torna la dimensione della lista- Iterator <E> iterator(); torna l'iteratore; l'iterator ha 3 metodi: hasNext(), next() e remove() (quest'ultimo non va quasi mai implementato; se va implementato in presenza di collezione immutabile bisogna che torni/lanci

Page 10: Lezione20-21 PO Mod1 Roncato

l'eccezione in modo tale che sia esplicito che l'applicazione che abbiamo scritto non riesca a togliere l'elemento. I primi due metodi in presenza di collezione immutabili si implementano in modo standard)

Quindi abbiamo visto come si concatenano due liste immutabili, ma come faccio a creare una lista? Prima abbiamo visto che il costruttore è privato, in realtà questo tipo di strutture dati viene implementato di solito con l'utilizzo di una convenzione che si chiama Design Pattern Null Object. L'idea di base è implementare la stessa interfaccia con due classi: una classe che si occupa di quando la "cosa" è vuota (con i vari metodi) e una classe che si occupa di quando una cosa "non è vuota" e quest'ultima richiama dentro di essa e nei metodi le cose definite nella classe "vuota". " il Null Object è una tecnica che suddivide l'implementazione di una collezione tra collezione senza elementi e collezione con elementi. Dal punto di vista esterno questa suddivisione non si nota, perché sia la collezione vuota che quella con elementi implementano un'interfaccia comune. La ragione della suddivizione è di rendere più semplice l'implementazione dei vari metodi".

Le liste, gli alberi, gli insiemi e la maggior parte delle strutture dati hanno un tipo particolare di oggetto che è quello vuoto (infatti abbiamo anche il metodo isEmpty() che può essere true o false).Quindi invece di avere l'interfaccia List<E> implementata con LinkedList<E> o ArrayList<E> o altro tipo modificabile, quando si adotta il Null Object con gli immutabili (vogliamo collezioni immutabili) definiti dal programmatore si ha una situazione del tipo: ho una interfaccia (ad esempio List<E>) e poi ho due implementazioni di questa interfaccia:- Null+nome interfaccia: nel nostro esempio si chiamerà NullList, che servirà a rappresentare e solo quando la lista è vuota - NotNull+nome interfaccia: nel nostro esempio si chiamarà NotNullList; di solito è chiamata NotNull..., ma può prendere anche altri nomi.Perchè fare questa differenza? Perchè la classe NullList (scrivo NullList in riferimento degli esempi successivi, ma avremo NullQueue e il rispettivo NotNullQueue, ecc) e i suoi metodi sono facili da da implementare, ad esempio;

class NullList<E> implements List<E>

public boolean isEmpty(){return true;

}

public int Size(){return 0; // siamo nella NullList

}

...... //l'itetatore ritorna un insieme vuoto}

Poi c'e' la classe NotNulllList in cui i metodi sono implementati in modo opportuno

Class NotNullList<E> implements List<E> {E info;Lista<E> next;.......

}

Questo pattern è un modo per risolvere problemi che viene molto usato e (nel nostro esempio) la stessa interfaccia Lista è implementata da due classi: una classe che serve a rappresentare la lista vuota e una classe che rappresenta una lista non vuota. L'implementazione della classe della lista è semplice, perchè in pratica non fa nulla; mentre l'implementazione della lista non vuota è più complessa. In tale visione non si definisce neanche il metodo Concatena visto prima, cioè non verrebbe fatto nel modo detto prima , ma quando dobbiamo costruire una lista, ad esempio definisco una lista con 1 elemento

Lista<Integer> l = new NotNullList(1, new NullList()); //passo comeprimo parametro il //primo dato (ad esempio un numero) e come secondo parametro "next" passo un elemento nullo)

Questo è un modo diverso di affrontare il problema delle collezioni ed è orientata più verso la programmazione funzionale, e qua gli effetti collaterali non ci sono.Se dovessi fare una lista con due elementi:

Lista<Integer> l2 = new NotNullList (2, l); // come secondo parametro passo la lista di prima

Page 11: Lezione20-21 PO Mod1 Roncato

//che aveva come unico elemento il numero 1 e ora l2 ha come contenuto: {2,1}

Questo stile di programmazione evita alcuni problemi: nel metodo Concatena di prima abbiamo dovuto distinguere fra "next uguale a null" e "next diverso da null". Se facciamo il Concatena ora, devo definire il Concatena sia per una lista nulla e sia per una non nulla, e se ho una lista nulla e ci concateno una lista non nulla, è molto semplice:

- metodo Concatena per NullList: mettere davanti ad una lista, la lista nulla, oppure concatenare una lista nulla con un'altra nulla è come avere solo la lista originaria, perchè appunto la lista nulla non ha niente al suo interno:

Lista<E> Concatena (Lista <? extends E> l){ //versione per NullList, e lo uso solo per NullListreturn l;}

- metodo Concatena per NotNullList:

Lista<E> Concatena (Lista <? extends E> l){return new NotNullList(info, next.Concatena(l)); //info è la informazione non nulla, e con next

//faccio quello che facevo prima con il metodo Concatena, ma non mi devo più preoccupare se next è //nullo perchè in ogni caso l'ultimo elemento non è nullo, ma è un oggetto NullList, e quindi nell'ultimo //passo viene invocato il Concatena della versione NullList

}

In questo modo sono stati semplificati notevolmente i metodi al costo di aggiungere una classe; questa cosa è stata fatta col polimorfismo: aggiungendo classi abbiamo metodi più semplici. Quindi se voglio sfruttare il poliformismo anche nella gestione di questi oggetti, la versione immutabile dell'oggetto semplifica molto i metodi, perchè ogni volta mi concentro su un caso più piccolo e invece di pensare a tutte le liste penso a cosa accade nella lista nulla e a cosa succede nella lista non nulla e ho i metodi più semplici.Queste stesse capitano se penso al metodo Size() (nella versione NotNullList)

int Size() {return 1 + next.Size();

}

Se next.Size() è NotNull allora avrà anche lui dentro di se in modo ricorsivo questa chiamata finchè l'ultimo elemento sarà Null che ovviamente mi ritornerà 0 e tutto è fatto senza il costrutto if... Se dovessi fare la stessa cosa per una lista che è nulla o non nulla, la cosa è più complessa.

In questa filosofia il costruttore sarà solo nella classe non vuota ed esso non sarà mai vuoto: nel caso NonNullList il costruttore é (non è mai vuoto) e come parametri deve avere la info e il next (mentre nella filosofia di prima dovevo avere sia il costruttore per la lista vuota che non vuota):

NotNullList(E Info, Lista<? extends E> next){this.info= info;this.next= next;

}

Implementare i metodi con questa logica li rende più corti e semplici. Questo modo di programmare aiuta a concentrarsi nei casi limite.

Page 12: Lezione20-21 PO Mod1 Roncato

Grafica (come i linguaggi ad oggetti gestiscono la grafica), il codice sarà poi messo online (da visionare e provare: lezione21.zip)

All'inizio Java aveva una libreria grafica denominata awt (applet da girare su un browser, ed era una libreria limitata), poi arrivò la libreria swing che era più avanvata (in swing abbiamo thread in esecuzione in parallelo)L'interfaccia grafica è rapprensentata da una finestrella con pulsanti, riga dove possiamo inserire dellinput, immagini ecc:

Normalmente sappiamo che per eseguire un programma java abbiamo il main() come punto di partenza; nella grafica invece, ed inoltre, abbiamo un qualcosa di nuovo che sono gli eventi (l'utente clicka da qualche parte e il programma fa qualcosa in base al codice scritto) e vi è la presenza di più thread, in totale 3, di cui i due più importanti sono:- un thread di inizializzazione, che è chiamato nel main, che parte e "disegna la finestrella, i pulsanti, il checkbox ecc" e poi passa al thread secondario il testimone (tale thread può poi morire o continuare a fare altre cose); qui il programmatore perde il controllo di quello che fa l'utente.- un thread secondario è quello dello Swing, il quale è "schiavo" dell'utente, cioè l'utente usa il mouse per selezionare o clickare, questo genera una azione che viene gestita dal thread con una libreria già pronta chiamata Swing, con del codice già scritto e che non si può modificare.

Con la parte grafica di java, entra in gioco la programmazione ad aventi. Un evento è un qualcosa che accade all'interno di una finestrella grafica, ad esempio vediamo la pressione di un determinato pulsante OK: il codice viene eseguito alla pressione usando un oggetto che si chiama Listener (che implementa una certa interfaccia) e che in qualche modo viene messo "in registrazione" al pulsante OK, di norma attraverso una classe anonima (e avremo un listener per un primo pulsante, un secondo listener diverso per un secondo pulsante e così via); dopo che è avvenuta la registrazione, ogni volta che l'utente preme il pulsante, automaticamente Swing invoca quel metodo implementato dall'interfaccia Listener; quando abbiamo finito ritorna a fare le sue cose.

Passi:1. il pulsante è premuto2. Swing (è un thread parallelo) visualizza l'animazione della pressione3. chiama il metodo onAction del Listener (vi sono tanti altri metodi, onAction è solo un esempio)4. scriviamo le istruzioni che vogliamo che vengano fatte, quando il metodo finisce, prosegue il thread dello swingFino al punto 3 lo swing rimane bloccato e dobbiamo gestire questa cosa, soprattutto se l'esecuzione delle istruzioni richiede molto tempo (in questo caso, dobbiamo creare un altro thread che le gestisca, altrimenti la parte grafica rimane bloccata troppo a lungo, ed è una cosa brutta da vedere).

L'inizializzazione della parte grafica può essere fatta nel main o in una classe a parte//init

public stati void main (....){final JFrame frame = new JFrame(); //creo la finestrella (contenitore) e ci do un titoloframe.setTitle("Test");JButton click = new JButton("ok); //aggiungo il pulsante che chiamo Clickframe.setLayout(new FlowLayout());//aggiundo un layout al frame in modo da avere una

//disposizione estitica migliore dei vari oggetti// Il FlowLayout permette di disporre i componenti di un contenitore da sinistra verso destra su un’unica linea.

frame.add(click); // aggiungo il pulsante alla finestrella

final JTextField() nome = new JTextField("nome");final JTextField() numero = new JTextField("secondi");frame.add(nome);frame.add(numero);... problema 1... problema 2

frame.setVisibile(true); // appare la finestra e il thread Swing vive di vita propria; a questo //punto main può anche finire, ma la finestra rimane visibile e attiva

Page 13: Lezione20-21 PO Mod1 Roncato

Problema 1: premo il pulsante ok e non capita nulla, perchè non ho fatto la "registrazione". Quindi devo creare Listener e far si che il pulsante ok faccia qualcosa

ActionListener listener = new ActionListener { //uso una classe anonima@overridepublic void actionPerformer(ActionEvent e){ //actionPerformer è l'unico metodo di ActionListener; uso e per capire quale è stato //l'evento che ha innescato la chiamata (in questo caso sappiamo che è innescato dalla //pressione del pulsante OK; in presenza di più listener, sarà e a dirci cosa ha fatto cominciare //l'evento

//inizio istruzioni che vogliamo siano eseguite in seguito all'evento nome.setText(nome.getText().toUpper());

//e se il codice che scrivo dopo è di veloce esecuzione va bene, altrimenti dopo è veloce va //bene, altrimenti devo mettere il codice che gestisce le cose in un altro thread............

} } //tra tutti gli ascoltatori che vi sono, aggiungo anche questo che ho appena creato per il pulsante OK //quindi ora alla pressione del codice OK vengono eseguite le istruzioni scritte qua sopra

click.addAcionListener(listener);

Problema 2: come si chiude la finestrella?

// nel codice, prima di "visible" si mette questa istruzione per uscire, altrimenti il processo Swing rimane // in esecuzioneframe.setDefaultCloseOperation(javax.swing.WindowsConstants.EXIT_ON_CLOSE);

Ora fintanto che facciamo istruzioni veloci (qualche millisecondo) possiamo gestire l'applicazione come abbiamo visto.Di seguito vi è un esempio di applicazione non veloce e vediamo come gestirla con la parte grafica; supponiamo che quando premiamo un tasto, viene emesso un suono, il thread rimane fermo per un tot di secondi e poi si riemette un suono

static void longOperation (int secondi){java.awt.Toolkit.getDefaultToolkit().beep(); //fa un suonothread.sleep(secondi * 1000); //sleep richiede millisecondi come arg. quindi se

//vogliamo secondi, dobbiamo moltiplicare per 1000//qua vi sarà una eccezione da inserire per gestire sleep (dobbiamo inserirla noi)

java.awt.Toolkit.getDefaultToolkit().beep();//fa un suono per dire che //terminata l'operazione "lunga"

}

ira, per far funzionare longOperation, Il seguente codice va messo dentro actionPerformer:

{//leggo un numero che l'utente ha inserito e lo trasformo da testo a numeroint sec = Integer.parseInt(numero.getText());//il thread si ferma per tot secondi, e qualsiasi operazione che vada a fare nella finestrella in //questi secondi, non ha alcune effettolongOperation(sec))}

Page 14: Lezione20-21 PO Mod1 Roncato

Poichè longOperation potrebbe richiede del tempo e per quel tempo l'interfaccia non è utilizzabile , allora è meglio mettere longOperation in un altro thread che va in parallelo allo swing e perciò uso una struttura detta SwingWorker, fatta apposta per questo, che esiste già in java: facciamo tutto dentro il codice che stiamo scrivendo, ma usando una classe anonima.

//da notare che ora uso una classe anonimaSwingWorker worker = new SwingWorker<Void, Void>() {

@Override //il metodo è invocato da un thread diverso da swing public void doInBackground() { longOperation(sec); return .......... }

.........//una volta creato il worker, lo facciamo partire con execute()

worker.execute();

Ricapitolando una volta premuto OK, viene eseguito del codice, che prepara l'input per il metodo, poi crea l'oggetto Worker , che estende la classe SwingWorker andando a cambiare un metodo particolare che si chiama doInBackground (che viene fatto in background su un altro thread) e quindi l'interfaccia grafica non viene bloccata, perchè il codice è eseguito in un altro thread separato e parallelo a quello di Swing. Quando il worker è stato completato viene chiamato un altro metodo per segnalare la fine del worker. Per questioni di efficienza tutta l'interfaccia Swing è delicata dal punto di vista della concorrenza (dei thread) e questo comporta il fatto che non si accede in modo concorrente a due oggetti dell'interfaccia Swing. Quindi: quando c'e' un actionPerformer viene eseguito dal thread Swing, e quando viene eseguito doInBackground, quest'ultimo è eseguito da un altro thread separato che è appunto il worker. Il thread Swing può accedere a tutti i vari numero.getText, numero.setText, ecc cioè a tutti gli oggetti, perchè sono cose del thread Swing; il thread worker non dovrebbe mai accedere a nessun oggetto dell'interfaccia Swing. Per tale ragione doInBackground viene fatto su un thread a parte e questo thread deve operare solo sui suoi dati (che non riguardano l'interfaccia). Quando worker termina, ho un metodo "done", richiesto dallo swing, per sapere quando worker ha terminato. Worker ha tutta una serie di metodi che hanno lo scopo di preservare le cose dell'interfaccia grafica.

Ci sono tutta una serie di eventi a più basso livello (mouse clickato, pressione di un tasto, ecc) che possono essere intercettati con dei listener diversi (ad esempio mouseListener) per gestire meglio molti aspetti della programmazione o della gestione di possibili errori di input (ad esempio in un campo dove ci si aspetta un numero, un utente può inserire una stringa vuota, o del testo, ecco che allora viene lanciata una eccezione per gestire cose anomale).