SETTIMANA 9 Accedere a nodi di una catena Iteratori e iteratore in una catena (capitolo 14) 1 Per eseguire operazioni sulla catena che necessitano di accedere ai nodi è necessario aggiungere metodi all’interno della classe LinkedList che è l’unica ad avere accesso ai nodi della catena Potremmo volere, ad esempio Contare i nodi nella catena verificare la presenza di un particolare oggetto nella catena (algoritmo di ricerca) aggiungere/togliere elementi alla catena, anche in posizioni intermedie Questo limita molto l’utilizzo della catena come struttura dati definita una volta per tutte… 2 Accedere a nodi di una catena Accedere a nodi di una catena Vogliamo che la catena fornisca uno strumento per accedere ordinatamente a tutti i suoi elementi. Idea: scriviamo un metodo getHead che restituisce un riferimento al nodo header Esempio: vogliamo contare i nodi della catena È necessario scorrere tutta la catena Il codice va scritto dentro la classe LinkedList public class LinkedList { ... public int getSize() { ListNode temp = head.getNext(); int size = 0; while (temp != null) { size++; temp = temp.getNext(); } //se la catena è vuota size = 0 (giusto!) return size; } .... } public class LinkedList {... public ListNode getHead() { return head; } ...} 3 Iteratore in una catena Questo approccio però non funziona Se la classe ListNode è public in LinkedList si viola l'incapsulamento, perché diventa possibile modificare lo stato della catena dall'esterno Se invece è private allora i nodi sono inaccessibili e il metodo getHead è totalmente inutile 4 Iteratore in una catena Soluzione del problema fornire all’utilizzatore della catena uno strumento con cui interagire con la catena per scandire i suoi nodi Tale oggetto si chiama iteratore e ne definiamo prima di tutto il comportamento astratto Un iteratore rappresenta in astratto il concetto di posizione all’interno di una catena Un iteratore si trova sempre dopo un nodo e prima del nodo successivo (che può non esistere se l’iteratore si trova dopo l’ultimo nodo) ITERATORE inizio (head) fine (tail) null null header • In particolare, quando viene creato l’iteratore si trova dopo il nodo header 5 dato dato dato Un iteratore rappresenta in astratto il concetto di posizione all’interno di una catena la posizione è rappresentata concretamente da un riferimento ad un nodo (il nodo precedente alla posizione dell’iteratore) 6 L'ADT ListIterator import java.util.NoSuchElementException; public interface ListIterator { /*Una classe che realizza ListIterator per una catena avra` un costruttore del tipo NomeClasse(ListNode h), che crea un iteratore che si trova subito dopo il nodo h*/ /*Lancia NoSuchElementException se l'iteratore e` alla fine, altrimenti restituisce l'oggetto che si trova dopo la pos. attuale e sposta l’iteratore di una pos. in avanti*/ Object next() throws NoSuchElementException; L'ADT ListIterator Possiamo immaginare un iteratore come un cursore in un elaboratore di testi Un nodo della catena corrisponde ad un carattere L’iteratore si trova sempre “tra due nodi”, come un cursore /*verifica se si puo` invocare next()*/ boolean hasNext(); /*inserisce x in prima della posizione attuale, senza modificare la posizione dell'iteratore*/ void add(Object x); } /*Lancia IllegalStateException se invocato 2 volte consecutive altrimenti elimina l'ultimo oggetto esaminato da next() o inserito da add() senza modificare la pos. dell’iteratore*/ void remove() throws IllegalStateException; 7 8 Iteratore in una catena L’interfaccia Iterator in Java A questo punto, è sufficiente che la catena fornisca un metodo per creare un iteratore La libreria standard di Java definisce in java.util una interfaccia Iterator Definisce in particolare i metodi next e hasNext Sempre in java.util si trova l’interfaccia ListIterator Estende Iterator Definisce altri metodi, tra cui add e remove L’interfaccia Iterator è implementata da una classe che abbiamo utilizzato molto spesso: Scanner Ha il metodo next Ha il metodo hasnext public class LinkedList { ... public ListIterator getIterator() { ... }// dopo vediamo come scrivere questo metodo ... } E si può scandire la catena senza accedere ai nodi LinkedList list = new LinkedList(); ... ListIterator iter = list.getIterator(); while(iter.hasNext()) System.out.println(iter.next()); // notare similitudine con StringTokenizer e Scanner 9 10 La classe interna LinkedListIterator Implementare ListIterator public class LinkedList { ... //codice di LinkedList come prima ... //incluso codice della classe privata ListNode getIterator restituisce un riferimento ad una interfaccia public ListIterator getIterator() //metodo di LinkedList { //crea un iteratore posizionato al primo nodo della catena return new LinkedListIterator(head); } Quindi in realtà deve creare un oggetto di una classe che realizzi tale interfaccia Implementiamo ListIterator con la classe LinkedListIterator I suoi oggetti sono costruiti solo all’interno di LinkedList e restituiti all’esterno solo tramite riferimenti a ListIterator Per un corretto funzionamento dell’iteratore occorre concedere a tale oggetto il pieno accesso alla catena • in particolare, alla sua variabile di esemplare head Non vogliamo che l’accesso sia consentito ad altre classi Tutto questo ci porta a definire LinkedListIterator come una classe interna privata di LinkedList 11 } private class LinkedListIterator implements ListIterator { //costruttore public LinkedListIterator(ListNode h) { current = h; previous = null; } //metodi pubblici (dobbiamo ancora realizzarli) public boolean hasNext() { ... } public Object next() { ... } public void add(Object x) { ... } public void remove() { ... } //campi di esemplare private ListNode current;//nodo che precede pos. attuale private ListNode previous;//nodo che precede current } 12 I metodi di LinkedListIterator import java.util.NoSuchElementException; public class LinkedList { ... private class LinkedListIterator implements ListIterator { ... //metodi pubblici hasNext, next public boolean hasNext() { return current.getNext() != null; } public Object next() { if (!hasNext()) throw new NoSuchElementException(); previous = current; current = current.getNext(); return current.getElement(); } //metodi pubblici add,remove (dobbiamo ancora realizzarli) public void add(Object x) { ... } public void remove() { ... } //campi di esemplare private ListNode current;//nodo che precede pos. attuale private ListNode previous;//nodo che precede current } } Il metodo add di LinkedListIterator public void add(Object obj) { ListNode n = new ListNode(obj, current.getNext()); current.setNext(n); previous = current; //aggiorno riferimenti iteratore current = current.getNext(); //a subito dopo nuovo nodo if (!hasNext()) // se ho aggiunto all'ultimo nodo LinkedList.this.tail = current; //aggiorno tail } Aggiunge il nuovo nodo e avanza di una posizione Se il nodo viene aggiunto alla fine della catena, allora bisogna anche aggiornare il riferimento tail della catena Nota sintattica: il riferimento LinkedList.this punta all'oggetto LinkedList all'interno di cui è stato creato l'iterator 13 14 Il metodo add di LinkedListIterator Il metodo remove di LinkedListIterator ITERATORE (inizio esecuzione) obj inizio (head) fine (tail) null null header dato ITERATORE (fine esecuzione) dato dato 15 /* Va sempre invocato dopo add o next, anche la prima volta */ public void remove() throws IllegalStateException { /*Se dalla costruzione dell'iteratore o dall'ultima invocazione di remove non e` stato invocato next o add, allora previous==null. Stato illegale! */ if (previous == null) throw new IllegalStateException(); previous.setNext(current.getNext()); current = previous; // aggiorno riferimenti iteratore previous = null; // a subito prima del nodo rimosso if (!hasNext()) // se ho rimosso l'ultimo nodo LinkedList.this.tail = current; //aggiorno tail } L'iteratore non ha un riferimento al nodo prima di previous Quindi non posso aggiornare correttamente previous dopo la rimozione: gli assegno valore null, con la conseguenza che l'iteratore si trova in uno stato illegale dopo la rimozione Per questo motivo non si può invocare nuovamente remove. Invocando next o add previous verrà riaggiornato 16 Il metodo remove di LinkedListIterator ITERATORE (fine esecuzione) null inizio (head) fine (tail) null null header Operazioni su catene mediante un iteratore ITERATORE (inizio esecuzione) dato dato dato dato 17 18 Catena: conteggio elementi Problema: data una catena ed un riferimento ad essa, si vuole sapere il numero di nodi che la compongono Soluzione: con un iteratore possiamo contare i nodi al di fuori del codice di LinkedList, in una classe qualsiasi public class MyClass { public static void main(String[] args) { LinkedList list = new LinkedList(); ... // operazioni sulla catena int listSize = getSize(list); } public static int getSize(LinkedList list) { ListIterator iter = list.getIterator(); int size = 0; while (iter.hasNext()) { size++; iter.next(); // ignoro l’oggetto ricevuto } // perché devo solo contarli return size; } } Catena: inserimento e rimozione Abbiamo visto l’inserimento e la rimozione di un elemento all’inizio e alla fine della catena, tramite i metodi addFirst, removeFirst, addLast, removeLast Vogliamo estendere le modalità di funzionamento della catena per poter inserire e rimuovere elementi in qualsiasi punto della catena stessa abbiamo il problema di rappresentare il concetto di posizione nella catena in cui inserire (o da cui rimuovere) il nodo Possiamo usare di nuovo l’iteratore, e in particolare i suoi metodi add e remove 19 20 Catena: osservazioni finali Ci sono alcune differenze tra la nostra realizzazione della catena e la realizzazione della classe LinkedList proposta sul libro di testo Usiamo un primo nodo “vuoto” “Sprechiamo” dello spazio in memoria Ma semplifichiamo la realizzazione dei metodi (non bisogna trattare i casi limite diversamente dagli altri) Usiamo un riferimento tail all’ultimo nodo Questo migliora le prestazioni di addLast senza peggiorare le altre e non richiede molto spazio di memoria Catena doppia (doubly linked list) 21 22 Catena doppia Catena doppia inizio (head) null null ogni nodo è un oggetto che contiene un riferimento ad un elemento (il dato) un riferimento al nodo successivo della lista (next) un riferimento al nodo precedente della lista (prev) 23 prev prev prev next next next null null La catena doppia (o lista doppiamente concatenata, doubly linked list) non è un nuovo ADT Come la catena, è una struttura dati per la realizzazione di ADT Una catena doppia è un insieme ordinato di nodi fine (tail) dato dato Dato che la struttura è simmetrica, si usano due nodi vuoti, uno a ciascun estremo della catena Tutto quanto detto per la catena (semplice) può essere agevolmente esteso alla catena doppia Il metodo removeLast diventa O(1) come gli altri metodi Un iteratore su catena doppia avrà anche un metodo per retrocedere di una posizione, oltre a quello per avanzare i metodi add e remove dell’iteratore si complicano 24 Lista Il concetto di iteratore su catena, esplicitato nell'interfaccia ListIterator, definisce il comportamento astratto di un contenitore in cui i dati sono disposti in sequenza (cioè per ogni dato è definito un precedente ed un successivo) nuovi dati possono essere inseriti in ogni punto dati possono essere rimossi da qualsiasi punto Un contenitore con un tale comportamento può essere molto utile, per cui si definisce un tipo di dati astratto, detto lista, con la seguente interfaccia Il tipo di dato astratto Lista 25 Lista public interface List extends Container { ListIterator getIterator(); } 26 Collaudo di List e LinkedList (1) Attenzione a non confondere la lista (che è un ADT) con la lista concatenata o catena (che è una struttura dati) La classe LinkedList può essere usata per realizzare l'interfaccia List: public class LinkedList implements List { ... // codice tutto come prima } ma non è necessario realizzare una lista mediante una catena, perché nella definizione della lista non vengono menzionati i nodi Ad esempio è possibile definire una lista che usa un array come struttura di memorizzazione dei dati public class SimpleListTester1 { public static void main(String[] args) { LinkedList list = new LinkedList(); //posso usare tutti i metodi di LinkedList list.addFirst("A"); //A list.addLast("B"); //AB list.addFirst("C"); //CAB ListIterator iter = list.getIterator(); iter.next(); iter.add("I"); //CIAB while (iter.hasNext()) iter.next(); iter.remove(); //CIA list.addLast("O"); //CIAO iter = list.getIterator(); while (iter.hasNext()) C System.out.println(iter.next()); } I } A O 27 28 Collaudo di List e LinkedList (2) public class SimpleListTester2 { public static void main(String[] args) { List list = new LinkedList(); //posso usare solo il metodo getIterator di List ListIterator iter = list.getIterator(); iter.add("A"); //A iter.add("B"); //AB Liste posizionali e liste con rango (vettori) iter = list.getIterator(); iter.add("C"); //CAB iter.add("I"); //CIAB while (iter.hasNext()) iter.next(); iter.remove(); //CIA iter.add("O"); //CIAO } } iter = list.getIterator(); while (iter.hasNext()) System.out.println(iter.next()); C I A O 29 30 Gli ADT “lista” e “vettore” Rango, posizione, liste, vettori Le operazioni più generali che vogliamo effettuare su una lista riguardano l’inserimento e l’eliminazione di un nuovo elemento in una posizione qualsiasi La posizione di un elemento nella lista può essere indicata in modo relativo attraverso un iteratore indicata in modo assoluto attraverso il rango di un elemento Il rango è un indice che possiamo definire ricorsivamente ha valore 0 per il primo elemento della lista ha valore i+1 per l’elemento che segue l’elemento di rango i Conveniamo di chiamare vettore una lista con rango Il tipo di dato astratto “vettore” e il tipo di dato astratto “lista” hanno astrazioni diverse Un vettore è una sequenza ordinata di dati, ciascuno accessibile mediante un indice intero (il rango) Un vettore consente quindi l’accesso casuale a tutti gli elementi tramite l’uso di indici Una lista è una sequenza ordinata di dati, accessibili in sequenza mediante un iteratore Una lista consente solamente accesso sequenziale ai propri elementi 31 32 Vettori e liste: efficienza operazioni L'ADT “vettore” L'ADT vettore è una generalizzazione del concetto di array: Ha una lunghezza variabile È possibile aggiungere e togliere elementi in qualsiasi posizione del vettore È possibile accedere (in lettura e scrittura) al valore di un elemento noto il suo rango Un modo naturale per realizzare l'ADT vettore è quello di utilizzare un array riempito solo in parte In java.util esiste la classe ArrayList, che realizza una lista con rango e con una ricca dotazione di funzionalità L'efficienza delle operazioni fondamentali cambia se la posizione è specificata tramite rango o iteratore Con rango: l'accesso richiede tempo costante, ma inserimenti e rimozioni comportano in media lo spostamento di n/2 elementi, cioè tempo O(n) Con iteratore: l'accesso richiede tempo lineare O(n) (bisogna scorrere la lista dall'inizio), ma inserimenti e rimozioni comportano un numero costante di operazioni 33 34 35 36 Riassunto: dati in sequenza Abbiamo fin qui visto diversi tipi di contenitori per dati in sequenza, rappresentati dagli ADT pila coda lista con iteratore lista con rango (vettore) Per realizzare tali ADT, abbiamo usato diverse strutture dati Array Catena Catena doppia Mappa: definizione Mappe e Dizionari (capitolo 15 ­ ma la nostra trattazione è abbastanza diversa) Una mappa è un ADT con le seguenti proprietà Contiene dati (non in sequenza) che sono coppie di tipo chiave/valore Non può contenere coppie con identica chiave: ogni chiave deve essere unica nell’insieme dei dati memorizzati Consente di inserire nuove coppie chiave/valore Consente di effettuare ricerca e rimozione di valori usando la chiave come identificatore 37 L’interfaccia Dictionary Dizionario: definizione L'ADT dizionario ha molte similitudini con l'ADT mappa Valgono tutte le proprietà dell'ADT mappa, tranne una Non si richiede che le chiavi siano uniche nel dizionario C'è analogia con un dizionario di uso comune, in cui le chiavi sono le singole parole I valori sono le definizioni delle parole nel dizionario le chiavi (parole) possono essere associate a più valori (definizioni) e quindi comparire più volte nel dizionario la ricerca di un valore avviene tramite la sua chiave Si distinguono dizionari ordinati e dizionari non­ordinati A seconda che sull'insieme delle chiavi sia o no definita una relazione totale di ordinamento, cioè (in Java) che le chiavi appartengano ad una classe che implementa Comparable La nostra trattazione è limitata ad un caso ben preciso Dizionari ordinati a chiave unica (cioè mappe) 39 Implementare Dictionary con array Un dizionario può essere realizzato usando la struttura dati array Ogni cella dell’array contiene un riferimento ad una coppia chiave/valore La coppia chiave/valore sarà un oggetto di tipo Pair (da definire) Generalmente si usa un array riempito solo in parte A seconda degli ambiti applicativi ci sono due strategie possibili mantenere le chiavi ordinate nell’array mantenere le chiavi non ordinate nell’array A seconda della strategia scelta, cambiano le prestazioni dei metodi del dizionario 38 public interface Dictionary extends Container { // l'inserimento va sempre a buon fine; se la chiave non // esiste la coppia viene aggiunta al dizionario. Se esiste, // il valore ad essa associato viene sovrascritto dal nuovo // valore; se key e` null si lancia IllegalArgumentException void insert(Comparable key, Object value); // la rimozione della chiave rimuove anche il corrispondente // valore dal dizionario. Se la chiave non esiste si lancia // DictionaryItemNotFoundException void remove(Comparable key); } // la ricerca per chiave restituisce solo il valore ad essa // associato nel dizionario. Se la chiave non esiste si // lancia DictionaryItemNotFoundException Object find(Comparable key); //Eccezione che segnala il mancato ritrovamento di una chiave class DictionaryItemNotFoundException extends RuntimeException { } 40 Dizionario con array ordinato Se le n chiavi vengono conservate ordinate nell’array La ricerca ha prestazioni O(log n) Perché si può usare la ricerca per bisezione la rimozione ha prestazioni O(n) Perché bisogna effettuare una ricerca, e poi spostare mediamente n/2 elementi per mantenere l’ordinamento L’inserimento ha prestazioni O(n) Perché si può usare l’ordinamento per inserimento in un array ordinato Usando un diverso algoritmo occorre riordinare l’intero array, con prestazioni almeno O(n log n) 41 42 Dizionario con array non ordinato Prestazioni di un dizionario Se le n chiavi vengono conservate non ordinate La ricerca ha prestazioni O(n) Bisogna usare la ricerca lineare La rimozione ha prestazioni O(n) Bisogna effettuare una ricerca (lineare), e poi spostare nella posizione trovata l'ultimo elemento dell'array (l’ordinamento non interessa) L’inserimento ha prestazioni O(n) Bisogna rimuovere (sovrascrivere) un elemento con la stessa chiave, se c'è, e poi inserire il nuovo elemento nella ultima posizione dell’array (l’ordinamento non interessa) [Se non si richiede che le chiavi siano uniche nel dizionario, la rimozione non è necessaria e l'inserimento è O(1) ] 43 [O(1) per chiavi non uniche] La scelta di una particolare realizzazione dipende dall’utilizzo tipico del dizionario nell’applicazione Se si fanno frequenti inserimenti e sporadiche ricerche e rimozioni la scelta migliore è l’array non ordinato Se il dizionario viene costruito una volta per tutte, poi viene usato soltanto per fare ricerche la scelta migliore è l’array ordinato 44 La classe interna Pair Realizzazione di un dizionario 45 La classe ArrayDictionary public class ArrayDictionary implements Dictionary { public ArrayDictionary() { v = new Pair[INITSIZE]; // ... sempre uguale makeEmpty(); } public boolean isEmpty() { return vSize == 0; } // ... sempre uguale public void makeEmpty() { vSize = 0; } // ... sempre uguale public String toString() { String s = ""; for (int i = 0; i < vSize; i++) s = s + v[i] + "\n"; return s; } public void insert(Comparable key, Object value) { if (key == null) throw new IllegalArgumentException(); try { remove(key); } //elimina elemento se gia` presente catch (DictionaryItemNotFoundException e) {} //... ovvero sovrascrive elemento se gia` presente if (vSize == v.length) v = resize(2*vSize); v[vSize++] = new Pair(key, value); } //continua 47 public class ArrayDictionary implements Dictionary { ... protected class Pair //classe interna ad ArrayDictionary { public Pair(Comparable key, Object value) { setKey(key); setValue(value); } //metodi pubblici public String toString() { return key + " " + value; } public Comparable getKey() { return key; } public Object getValue() { return value; } public void setKey(Comparable key) { this.key = key; } public void setValue(Object value) { this.value = value; } //campi di esemplare private Comparable key; private Object value; } ... } 46 La classe ArrayDictionary //continua protected Pair[] resize(int newLength) //metodo ausiliario { ... } //solito codice public void remove(Comparable key) { v[linearSearch(key)] = v[--vSize]; } public Object find(Comparable key) { return v[linearSearch(key)].getValue(); } private int linearSearch(Comparable key) //metodo ausiliario { for (int i = 0; i < vSize; i++) if (v[i].getKey().compareTo(key) == 0) //oppure if (v[i].getKey().equals(key)), se il //metodo equals e` stato realizzato correttamente return i; throw new DictionaryItemNotFoundException(); } //campi di esemplare protected Pair[] v; protected int vSize; protected final static int INITSIZE = 10; protected class Pair { ... } // codice della classe Pair 48 Collaudo di un dizionario Dizionario con array ordinato Avendo usato un array non ordinato, i metodi remove e find effettuano una ricerca lineare sulle chiavi Esercizio: realizzare un dizionario con un array ordinato Il metodo insert deve mantenere ordinato l'array ad ogni inserimento (usando insertionSort...) I metodi remove e find possono usare la ricerca binaria per trovare una chiave Il metodo remove deve ricompattare l'array dopo la rimozione, mantenendolo ordinato public { // // // // } class SortedArrayDictionary extends ArrayDictionary realizzazione con array non ordinato. Eredita campi di esemplare e variabili statiche, la classe Pair, i metodi isEmpty, makeEmpty, resize. Deve sovrascrivere i metodi insert, remove, find import java.util.Scanner; import java.io.*; public class SimpleDictionaryTester { public static void main(String[] args) throws IOException { //creazione dizionario: leggo dati da file e assumo che //il file abbia righe nel formato <numero int> <stringa> Scanner infile = new Scanner(new FileReader("file.txt")); Dictionary dict = new ArrayDictionary(); // ... oppure = new SortedArrayDictionary(); while (infile.hasNextLine()) { Scanner linescan = new Scanner(infile.nextLine()); int key = Integer.parseInt(linescan.next()); String value = linescan.next(); dict.insert(key,value); //inserisco chiave e valore } infile.close(); //ricerca/rimozione dati nel dizionario Scanner in = new Scanner(System.in); boolean done = false; // continua 49 50 Collaudo di un dizionario } } while (!done) // continua { System.out.println("**** Stampa dizionario ****"); System.out.println(dict +"\nF=find,R=remove,Q=quit"); String cmd = in.nextLine();//ho sovrascritto toString if (cmd.equalsIgnoreCase("Q")) { done = true; } else if (cmd.equalsIgnoreCase("F")) { System.out.println("Chiave da trovare?"); int key = Integer.parseInt(in.nextLine()); try{ //cerca key chiave e restituisce il valore String value = (String)dict.find(key); System.out.println("Valore: " + value); } catch(DictionaryItemNotFoundException e) { System.out.println("Chiave non trovata");} } else if (cmd.equalsIgnoreCase("R")) { System.out.println("Chiave da rimuovere?"); int key = Integer.parseInt(in.nextLine()); try{//rimuove la coppia identificata da key dict.remove(key); System.out.println("Chiave rimossa"); } catch(DictionaryItemNotFoundException e) { System.out.println("Chiave non trovata");} } } Dizionari a chiavi numeriche L'ADT Tabella 51 52 Chiavi numeriche Tabella Supponiamo per un attimo che le chiavi di un dizionario siano numeri interi appartenenti ad un intervallo noto a priori Allora si può realizzare molto semplicemente un dizionario con prestazioni O(1) per tutte le operazioni Si usa un array che contiene soltanto i riferimenti ai valori delle coppie del dizionario Le chiavi delle coppie vengono usate come indici nell’array Le celle dell’array che hanno come indice una chiave che non è presente nel dizionario hanno il valore null Un dizionario con chiavi numeriche intere viene detto tabella o tavola (table) L’analogia è la seguente un valore è una riga nella tabella le righe sono numerate usando le chiavi alcune righe possono essere vuote (senza valore) chiave = 0 valore0 null chiave = 2 valore2 chiave = 3 valore3 null null 53 dato dato dato 54 Tabella L'interfaccia Table Una realizzazione dell'ADT tabella tramite array fornisce prestazioni ottime tutte le operazioni sono O(1) Prima limitazione: le chiavi devono essere numeri interi Seconda limitazione (più pesante): la tabella non utilizza la memoria in modo efficiente l’intervallo di variabilità delle chiavi deve essere noto a priori per dimensionare la tabella Definiamo il tipo di dati astratto Table con un comportamento identico al dizionario l’unica sostanziale differenza è che le chiavi non sono riferimenti a oggetti di tipo Comparable, ma sono numeri interi (che evidentemente sono confrontabili) Potremmo (non lo facciamo) realizzarlo usando array (questa volta non riempiti solo in parte!) • la memoria richiesta per contenere n dati dipende dal loro contenuto, in particolare dal valore della chiave massima public interface Table extends Container { void insert(int key, Object value); void remove(int key); Object find(int key); } Se le chiavi sono molto “disperse”, ovvero se il fattore di riempimento è piccolo, si ha grande spreco di memoria 55 • fattore di riempimento di una tabella = (numero di dati contenuti nella tabella) / (dimensione della tabella) 56 Tabella con ridimensionamento Se l’intervallo di variabilità delle chiavi non è noto a priori e si inserisce una chiave di valore superiore alla lunghezza dell’array bisogna ridimensionare l’array Un inserimento richiede un tempo O(n) ogni volta che è necessario un ridimensionamento La struttura dati “tabella hash con bucket” • Qui non si può utilizzare l’analisi ammortizzata perché non si può prevedere il valore della nuova chiave il ridimensionamento può avvenire ad ogni inserimento, e le prestazioni nel caso peggiore sono quindi O(n) Può essere necessario un array di milioni di elementi per contenere poche decine di dati 57 58 Tabella hash con bucket Introduciamo una nuova struttura dati che ci permette di superare entrambe le limitazioni di una tabella realizzata tramite array Limitazioni della tabella: le chiavi devono essere numeri interi in un intervallo prefissato Introdurremo una funzione di hash che assegna dei valori numerici (interi in un intervallo prefissato) a delle chiavi generiche Seconda limitazione della tabella: la memoria non viene utilizzata in maniera efficiente Useremo un array di bucket (“secchi”), cosicché ogni indice dell'array possa essere associato a più elementi del dizionario Funzioni di hash 59 60 Chiavi generiche e codici hash Prima limitazione della tabella: le chiavi devono essere numeri interi E se le chiavi non sono int ma sono invece oggetti arbitrari di tipo Object? Esempio: contenitore di oggetti di tipo PersonaFisica, la cui chiave è il codice fiscale (cioè è di tipo String) Vogliamo realizzare il dizionario tramite una tabella anche in questo caso Dobbiamo “tradurre” le chiavi in valori numerici Dobbiamo cioè progettare un codice hash il codice hash è una funzione che ha come dominio l’insieme delle chiavi generiche in esame e come codominio un sottoinsieme dei numeri interi Codice hash Il codice hash è quindi una funzione così definita: h1: {Chiavi} → {Interi} In Java la classe Object mette a disposizione il metodo hashCode che restituisce un int con buone proprietà di distribuzione uniforme Se il metodo hashCode non viene ridefinito, esso associa ad un oggetto un valore numerico calcolato in base all’indirizzo dell’oggetto in memoria L’esistenza di questo metodo rende possibile l’utilizzo di qualsiasi oggetto come chiave 61 Un codice hash per stringhe 62 Un codice hash per stringhe Il valore numerico associato ad una stringa è quindi calcolato in questo modo "ABC" ⇒ 'A'·base2 + 'B'·base1 + 'C'·base0 Come si può trasformare una stringa in un numero intero? Ricordiamo il significato della notazione posizionale nella rappresentazione di un numero intero Che valore utilizziamo per la base? Spesso si usa un numero primo come valore di base, perché questo “mescola” bene i valori 434 = 4·102 + 3·101 + 4·100 Ciascuna cifra rappresenta un numero che dipende dal suo valore intrinseco e dalla sua posizione • Ovvero stringhe diverse hanno con buona probabilità codici hash diversi Usiamo la stessa convenzione per una stringa, dove ogni carattere rappresenta un numero che dipende Dal suo valore intrinseco come carattere nella codifica Unicode Dalla sua posizione nella stringa In alternativa la base sarà (come per la notazione posizionale di interi) il numero di simboli diversi • che per la codifica Unicode è 216 = 65536 63 "ABC" ⇒ 'A'·655362 + 'B'·655361 + 'C'·655360 64 Mappe di compressione Mappe di compressione C'è ancora un problema: l’intervallo di variabilità delle chiavi deve essere noto a priori per dimensionare la tabella Affrontiamo il problema in questo modo Prefissiamo la dimensione della tabella in modo arbitrario Quindi definiamo di conseguenza l’intervallo di variabilità delle chiavi utilizzabili: il valore massimo consentito è la lunghezza della tabella Bisogna modificare la funzione h1 in modo che tutti i valori numerici associati alle chiavi ricadano all’interno dell’intervallo prefissato Si usa una ulteriore funzione di trasformazione delle chiavi, detta mappa di compressione 65 Abbiamo definito il codice hash h1: {Chiavi}→{Interi} In Java la funzione h1 è realizzata dal metodo hashCode Definiamo ora una funzione mappa di compressione: h2 : {Interi} → [0, N ­ 1] Il codominio di h2 è il sottoinsieme [0, N ­ 1] dei numeri interi, dove N coincide con la dimensione della tabella Esempio di mappa di compressione resto della divisione intera tra il codice hash di un oggetto e la dimensione N della tabella h2(h1(x)) = h1(x) % N Il valore calcolato è un intero nell’intervallo [0, N­1] 66 Funzioni di hash Funzioni di hash La funzione di hash h(x) come composizione di h1 e h2 Siamo ora in grado di definire una funzione di hash h(x) come composizione delle funzioni h1 (codice hash) e h2 (mappa di compressione) h : {Chiavi} → [0, N ­ 1] h(x) = h2(h1(x)) Una funzione di hash ha come dominio l’insieme delle chiavi che identificano univocamente i dati da inserire nel dizionario come codominio l’insieme degli indici validi per accedere ad elementi della tabella Chiavi arbitrarie Codice hash h1 ­3 ­2 ­1 0 1 2 3 Mappa di compressione h2 • il risultato dell’applicazione della funzione di hash ad una chiave si chiama chiave ridotta Una tabella che usa chiavi ridotte si chiama tabella hash (hash table) 67 0 1 2 3 …. N­1 68 Collisioni in una tabella hash Ora abbiamo un nuovo problema: Per come è definita, una funzione di hash è generalmente non biunivoca chiavi diverse possono avere lo stesso valore per la funzione di hash Bucket in una tabella hash • In generale non si può definire una funzione h biunivoca, perché il dominio ha dimensione maggiore del codominio Inseriamo nella tabella un elemento con chiave x1 Supponiamo che h(x1) = h(x2), dove x2 != x1 è la chiave di un elemento già presente nella tabella • Ovvero x1 ed x2 hanno la stessa chiave ridotta Il nuovo elemento sostituisce il vecchio nella tabella 69 • Questo non è corretto perché i due valori hanno, in realtà, chiavi diverse 70 Collisioni in una tabella hash Tabella hash con bucket Se due elementi hanno chiavi diverse ma uguali chiavi ridotte si verifica una collisione nella tabella In presenza di una collisione, bisognerebbe inserire il nuovo valore nella stessa cella della tabella (dell’array) che già contiene un altro valore ciascun valore deve essere memorizzato insieme alla sua vera chiave, per poter fare ricerche correttamente Possiamo risolvere il problema usando una lista associata ad ogni cella dell’array L’array è quindi un array di riferimenti a liste Ciascuna lista contiene le coppie chiave/valore che hanno la stessa chiave ridotta Questo sistema di risoluzione delle collisioni si chiama tabella hash con bucket un bucket è una delle liste associate ad una chiave ridotta hash = 0 bucket0 null hash = 2 bucket2 hash = 3 bucket3 null null 71 coppia coppia coppia coppia coppia 72 Tabella hash con bucket: prestazioni Le prestazioni della tabella hash con bucket non sono più, ovviamente, O(1) per tutte le operazioni Effettuiamo ad esempio una ricerca su una chiave x Il tempo per raggiungere il bucket di indice h(x) è O(1) Il tempo per raggiungere l’elemento con chiave x nel bucket dipende dalla lunghezza della lista Le prestazioni dipendono fortemente dalle caratteristiche della funzione di hash Una “buona” funzione di hash minimizza le collisioni, cioè fornisce chiavi ridotte distribuite in maniera uniforme nella tabella • In questo modo le liste in ogni bucket hanno tutte approssivamente la stessa lunghezza (minima) 73 Tabella hash con bucket: prestazioni Caso migliore: La funzione di hash restituisce chiavi ridotte che si distribuiscono uniformemente nella tabella Tutte le liste hanno la stessa lunghezza media Se N è la dimensione della tabella La lunghezza media di ciascuna lista è n/N Tutte le operazioni sono O(n/N) Tabella hash con bucket: prestazioni Caso peggiore La funzione di hash restituisce sempre la stessa chiave ridotta, per ogni chiave possibile Allora tutti i dati vengono inseriti in un’unica lista In questo caso le prestazioni della tabella hash degenerano in quelle di una lista • tutte le operazioni sono O(n) null null hash = 2 bucket2 null … coppia coppia coppia 74 Tabella hash con bucket: prestazioni Riassumendo, in una tabella hash con bucket si ottengono prestazioni ottimali, ovvero O(1), se La dimensione della tabella è circa uguale al numero di dati che saranno memorizzati nella tabella • Ovvero se il fattore di riempimento è circa unitario (così si riduce al minimo anche lo spreco di memoria) La funzione di hash genera chiavi ridotte che siano uniformemente distribuite • Ovvero produce liste di lunghezza quasi uguale alla lunghezza media • In particolare, se le chiavi vere sono uniformemente distribuite la funzione di hash h(x) = h1(x) % N genera chiavi ridotte uniformemente distribuite Per avere prestazioni O(1) occorre dimensionare la tabella in modo che N sia dello stesso ordine di grandezza di n 75 Sotto queste ipotesi le liste hanno quasi tutte lunghezza uno! 76 Il tipo di dati astratto Insieme (Set) Insiemi (capitolo 15 ­ ma la nostra trattazione è abbastanza diversa) È un contenitore (eventualmente vuoto) di oggetti distinti (cioè non contiene duplicati) Senza alcun particolare ordinamento o memoria dell’ordine in cui gli oggetti sono inseriti/estratti Corrisponde alla nozione matematica di insieme Definiamo la nostra astrazione di insieme tramite le seguenti operazioni inserimento di un oggetto • fallisce silenziosamente se l’oggetto è già presente verifica della presenza di un oggetto ispezione di tutti gli oggetti • restituisce un array (in generale non ordinato) di riferimenti agli oggetti dell’insieme Non definiamo un’operazione di rimozione 77 • useremo l'operazione di sottrazione tra insiemi (cfr. più avanti) 78 Insieme con array non ordinato Operazioni sugli insiemi Scriviamo innanzitutto l’interfaccia Set Per due insiemi A e B, si definiscono le operazioni unione, A ∪ B • appartengono all’unione di due insiemi tutti e soli gli oggetti che appartengono ad almeno uno dei due insiemi intersezione, A ∩ B La classe ArraySet ha questa interfaccia pubblica public class ArraySet implements Set { public void makeEmpty() { } public boolean isEmpty() { return true; } public void add(Object x) { } public boolean contains(Object x) { return true; } public Object[] toArray() { return null; } } • appartengono all’intersezione di due insiemi tutti e soli gli oggetti che appartengono ad entrambi gli insiemi sottrazione, A ­ B (oppure anche A \ B) • appartengono all’insieme sottrazione tutti e soli gli oggetti che appartengono ad A e non appartengono a B Abbiamo scritto enunciati return per metodi che non restituiscono void • non è necessario che B sia un sottoinsieme di A 79 La classe ArraySet • In questo modo la classe si compila da subito 80 Operazioni su insiemi: unione public class ArraySet implements Set { public ArraySet() { v = new Object[INITSIZE]; vSize = 0;} public void makeEmpty() { vSize = 0; } public boolean isEmpty() { return (vSize == 0); } public static Set union(Set s1, Set s2) { Set x = new ArraySet(); // inseriamo gli elementi del primo insieme Object[] v = s1.toArray(); for (int i = 0; i < v.length; i++) x.add(v[i]); // inseriamo tutti gli elementi del // secondo insieme, sfruttando le // proprietà di add (niente duplicati...) v = s2.toArray(); for (int i = 0; i < v.length; i++) x.add(v[i]); return x; } public void add(Object x)//prestazioni O(n) (usa contains) { if (contains(x)) return; if (vSize == v.length) v = resize(2*vSize); v[vSize++] = x; } public boolean contains(Object x) //metodo con prestaz. O(n) { for (int i = 0; i < vSize; i++) if (v[i].equals(x)) return true;//non si puo` usare return false; //compareTo perche` x e` solo un Object } public interface Set extends Container { void add(Object obj); boolean contains(Object obj); Object[] toArray(); } public Object[] toArray() // metodo con prestazioni O(n). { Object[] x = new Object[vSize]; //Creiamo un nuovo array System.arraycopy(v, 0, x, 0, vSize);//altrimenti si viola return x; } //l’incapsulamento private Object[] resize(int n) { ... }//solito codice //campi di esemplare e var. statiche private Object[] v; private int vSize; private static int INITSIZE = 100; Prestazioni: se contains è O(n) (e, quindi, lo è anche add), questa operazione è O(n2) 81 Operazioni su insiemi: intersezione public static Set intersection(Set s1, Set s2) { Set x = new ArraySet(); Object[] v = s1.toArray(); for (int i = 0; i < v.length; i++) if (s2.contains(v[i])) x.add(v[i]); // inseriamo solo gli elementi che // appartengono anche al secondo // insieme, sfruttando le proprieta’ // di add (niente duplicati...) return x; } 82 Operazioni su insiemi: sottrazione public static Set subtract(Set s1, Set s2) { Set x = new ArraySet(); Object[] v = s1.toArray(); for (int i = 0; i < v.length; i++) if (!s2.contains(v[i])) x.add(v[i]); // inseriamo solo gli elementi che // *non* appartengono al secondo // insieme, sfruttando le proprieta’ // di add (niente duplicati...) return x; } Prestazioni: se contains è O(n) l’operazione di intersezione è O(n2) Prestazioni: se contains è O(n) l’operazione di intersezione è O(n2) 83 84 Insieme con array non ordinato Riassumendo, realizzando un insieme con un array non ordinato le prestazioni di tutte le operazioni primitive dell’insieme sono O(n) le prestazioni di tutte le operazioni che agiscono su due insiemi sono O(n2) Esercizio: Insiemi di dati ordinabili Si può facilmente verificare che si ottengono le stesse prestazioni realizzando l’insieme con una catena (LinkedListSet) 85 86 87 public class ArraySortedSet implements SortedSet { public ArraySortedSet() { v = new Comparable[INITSIZE]; vSize = 0; } public void makeEmpty() { vSize = 0; } public boolean isEmpty() { return (vSize == 0); } public void add(Object x) //metodo di Set { throw new IllegalArgumentException(); } public void add(Comparable x) // prestazioni O(n) { ... } //Da completare: riordinamento per inserimento //E` O(n), perche' inseriamo in un array ordinato public boolean contains(Object x) //prestaz. O(log n) { ... } // da completare: usare ricerca binaria e compareTo public Comparable[] toSortedArray() // prestaz. O(n) { ... } //da completare (v e’ gia` ordinato...) public Object[] toArray() //Sovrascritto: l'array non deve { return toSortedArray(); } //essere per forza disordinato private Comparable[] resize(int newLength) //solito metodo { ... } // da completare //campi di esemplare e variabili statiche private Comparable[] v; private int vSize; private static int INITSIZE = 100; } 88 Esercizio: la classe ArraySortedSet Insieme di dati ordinabili Cerchiamo di capire se si possono avere prestazioni migliori quando l’insieme contiene dati ordinabili Definiamo l’interfaccia “insieme ordinato” public interface Set extends Container { void add(Object obj); boolean contains(Object obj); Object[] toArray(); } public interface SortedSet extends Set { void add(Comparable obj); Comparable[] toSortedArray(); } Realizziamo SortedSet usando un array ordinato dovremo definire due metodi add, uno dei quali impedisce l’inserimento di dati non ordinabili Operazioni su insiemi ordinati SortedSet: unione Gli algoritmi di unione, intersezione, sottrazione per insiemi generici possono essere utilizzati anche per insiemi ordinati infatti, un SortedSet è anche un Set Quale è la complessità dell'algoritmo di unione? Rimane O(n2) perché il metodo add è rimasto O(n), a causa del ri­ordinamento (con insertionSort) dell’array Sfruttiamo ciò che sappiamo delle realizzazioni di add e toSortedArray nella classe ArraySortedSet l’array ottenuto con il metodo toSortedArray è ordinato l’inserimento nell’insieme tramite add usa l’algoritmo di ordinamento per inserzione in un array ordinato Per realizzare l’unione, osserviamo che il problema è molto simile alla fusione di due array ordinati come abbiamo visto in mergeSort, questo algoritmo di fusione (che abbiamo realizzato nel metodo ausiliario merge) è O(n) L’unica differenza consiste nella contemporanea eliminazione (cioè nel non inserimento…) di eventuali oggetti duplicati un oggetto presente in entrambi gli insiemi dovrà essere presente una sola volta nell’insieme unione 89 90 SortedSet: unione SortedSet: unione Effettuando la fusione dei due array ordinati secondo l’algoritmo visto in MergeSort, gli oggetti vengono via via inseriti nell’insieme unione che si va costruendo Questi inserimenti avvengono con oggetti in ordine crescente Quali sono le prestazioni di add in questo caso? L’invocazione di contains ha prestazioni O(log n) per ogni inserimento L’ordinamento per inserzione in un array ordinato, usato da add, ha prestazioni O(1) per ogni inserimento! In questo caso add ha quindi prestazioni O(log n) Quindi complessivamente il metodo statico union ha prestazioni O(n log n) public static SortedSet union(SortedSet s1,SortedSet s2) { SortedSet x = new ArraySortedSet(); Comparable[] v1 = s1.toSortedArray(); Comparable[] v2 = s2.toSortedArray(); int i = 0, j = 0; while (i < v1.length && j < v2.length) // merge if (v1[i].compareTo(v2[j]) < 0) x.add(v1[i++]); else if (v1[i].compareTo(v2[j]) > 0) x.add(v2[j++]); else // sono uguali { x.add(v1[i++]); j++; } while (i < v1.length) x.add(v1[i++]); while (j < v2.length) x.add(v2[j++]); return x; } Quali sono le prestazioni di questo metodo union? 91 SortedSet: intersezione/sottrazione Quali sono le prestazioni dei metodi intersection e subtract se gli oggetti s1 ed s2 sono di tipo ArraySortedSet? L’invocazione s2.contains(v[i]) ha prestazioni O(log n) L'invocazione x.add(v[i]) ha in questo caso prestazioni O(log n). Vale infatti il ragionamento di prima: • L’invocazione di contains in add ha prestazioni O(log n) per ogni inserimento • L’ordinamento per inserzione in un array ordinato, usato da add, ha prestazioni O(1) per ogni inserimento! Quindi complessivamente i metodi statici intersection e subctract hanno prestazioni O(n log n) 92 Collaudo di Set e SortedSet import java.util.Scanner; import java.io.*; public class SimpleSetTester { public static void main(String[] args) throws IOException { //creazione degli insiemi: leggo dati da file e assumo //che il file contenga numeri interi, uno per riga Scanner file1 = new Scanner(new FileReader("ins1.txt")); Set insieme1 = new ArraySet(); //SortedSet insieme1 = new ArraySortedSet(); while (file1.hasNextLine()) insieme1.add(Integer.parseInt(file1.nextLine())); System.out.println("\n\n*** Insieme 1 ***"); printSet(insieme1); Scanner file2 = new Scanner(new FileReader("ins2.txt")); Set insieme2 = new ArraySet(); //SortedSet insieme2 = new ArraySortedSet(); while (file2.hasNextLine()) insieme2.add(Integer.parseInt(file2.nextLine())); System.out.println("\n\n*** Insieme 2 ***"); printSet(insieme2); file1.close(); file2.close(); 93 94 Collaudo di Set e SortedSet Collaudo di Set e SortedSet public static void printSet(Set s) { Object[] array = s.toArray(); //collaudo metodo toArray for (int i = 0; i < array.length; i++) System.out.print(array[i] + " "); System.out.println(); } //Collaudo metodi di unione, intersezione, differenza Set unione = union(insieme1, insieme2); //SortedSet unione = union(insieme1, insieme2); Object[] arrayUnione = unione.toArray(); System.out.println("\n\n*** Insieme Unione ***"); printSet(unione); public static Set union(Set s1, Set s2) { ... } //codice scritto prima Set intersezione = intersection(insieme1, insieme2); Object[] arrayInters = intersezione.toArray(); System.out.println("\n\n*** Insieme Intersezione ***"); printSet(intersezione); public static SortedSet union(SortedSet s1,SortedSet s2) { ... } //codice scritto prima Set differenza1 = subtract(insieme1, insieme2); Object[] arrayDiff1 = differenza1.toArray(); System.out.println("\n\n*** Insieme diff (1 - 2) ***"); printSet(differenza1); } public static Set intersection(Set s1, Set s2) { ... } //codice scritto prima public static Set subtract(Set s1, Set s2) { ... } //codice scritto prima Set differenza2 = subtract(insieme2, insieme1); Object[] arrayDiff2 = differenza2.toArray(); System.out.println("\n\n*** Insieme diff (2 - 1) ***"); printSet(differenza2); } 95 96 Riassunto: dati in sequenza Abbiamo visto diversi tipi di contenitori per dati in sequenza, rappresentati dagli ADT pila coda lista con iteratore lista con rango (vettore) dizionario insieme Per realizzare tali ADT, abbiamo usato diverse strutture dati array catena tabella hash (non la abbiamo realizzata) 97 98 Iscrizioni agli esami Primo appello: prima prova scritta 13­14 dicembre Sono disponibili sulle bacheche elettroniche le liste di iscrizione agli esami Scadenza iscrizioni: 11 dicembre Gli orari dei turni per la prima prova verranno comunicati il 12 dicembre sul sito del corso e sulle bacheche elettroniche Secondo appello: prima prova scritta 8­9 gennaio Istruzioni per l'esame 99 Chi si iscrive al primo appello potrà iscriversi anche al secondo (comunque vada...) Tuttavia è consigliabile iscriversi al primo appello solo se si è ragionevolmente sicuri di presentarsi 100 Non c’è solo la programmazione La prima prova scritta e l’esame orale vertono su tutto il programma Consultare il programma d’esame “ufficiale” sul sito del corso Studiare sul libro di testo e non sui lucidi! Questo corso non ha come unico scopo quello di insegnare a programmare, ma anche di fornire alcune basi teoriche importanti La prima prova scritta e, soprattutto, l’esame orale verificheranno l’apprendimento di questi argomenti … A maggior ragione domande che esulano da aspetti di pura programmazione verranno rivolte a coloro che sono bravi a programmare … 101 Preparazione alla prova di programmazione 102 Programmazione: consigli Preparazione alla prova scritta Il laboratorio 9 propone alcuni temi di esame (prove di programmazione) degli anni passati È fondamentale padroneggiare (in particolare) Tipi di dati astratti, e loro realizzazioni tramite opportune strutture dati Algoritmi di ordinamento e loro realizzazione, anche su strutture dati che non siano array Ereditarietà ed interfacce: implementare una interfaccia, estendere una classe, sovrascrivere metodi, conversioni di riferimenti … Input/output, anche su file: aprire file in lettura/scrittura, redirigere input/output, usare argomenti del metodo main … 103 Programmazione: consigli Concentrarsi prima sulla realizzazione di un prototipo funzionante nel rispetto delle specifiche Verificare accuratamente il funzionamento del programma, soprattutto in eventuali situazioni limite per velocizzare il collaudo si possono preparare file di input da usare con la redirezione, prevedendo l’output Dopo aver realizzato un prototipo funzionante, farne una copia così da poter ripristinare la versione corretta Modificare il programma in vista di eventuali obiettivi secondari Dopo ogni modifica, ri­effettuare un collaudo Se non si riesce a far funzionare la modifica, ripristinare la situazione corretta copiando il file 105 Obiettivi principali: realizzare un programma che rispetti le specifiche (senza fare cose in più…) che funzioni Come obiettivi secondari che sia efficiente che gestisca coerentemente gli errori che rispetti regole stilistiche e contenga commenti Leggere sempre con grande attenzione le specifiche Risolvere (eventualmente bene) un problema diverso da quello richiesto non serve a niente… Se si chiede di realizzare un metodo statico, bisogna realizzare un metodo statico! Se individuate una soluzione “migliore” che non usa un metodo statico… È sbagliata! 104 Programmazione: consigli Quando si scrive una classe, è comodo scrivere prima le firme di tutti i metodi, con un corpo “vuoto” public class ArraySet implements Set { public void makeEmpty() { } public boolean isEmpty() { return true; } public void add(Object x) { } public boolean contains(Object x) { return true; } public Object[] toArray() { return null; } } Invece di compilare tutto alla fine, si compila ogni volta che si scrive il corpo di un metodo in questo modo, si evita di trovarsi nella situazione in cui il compilatore segnala molti errori 106 Programmazione: consigli Per lo svolgimento della prova sarà consentito l'uso di alcune classi della libreria standard che verranno specificate nel testo di esame La documentazione della libreria standard sarà consultabile per tutta la durata della prova Si trova sul sito dell'Aula Taliercio ­> Guide On­Line ­> Java 6 docs ­> Java Platform API Specification Saper consultare rapidamente la documentazione in linea nel sito dell'ADT è molto importante: questa sarà la sola documentazione disponibile. In bocca al lupo 107 108