ALBERI E CLASSI GENERICHE Alberi e classi generiche Appunti di informatica per la classe 4° - articolazione informatica Prof. G. Malafronte Premessa ........................................................................................................................................................... 3 Alberi ................................................................................................................................................................. 3 Perché si utilizzano gli alberi?........................................................................................................................ 3 Alberi binari ................................................................................................................................................... 5 Alberi binari di ricerca - BST .......................................................................................................................... 6 Prestazioni di un albero binario di ricerca - BST ............................................................................................ 7 BST vs Array vs LinkedList .......................................................................................................................... 7 BST vs hashtable (hashmap) ...................................................................................................................... 8 Realizzazione di alberi binari di ricerca BST....................................................................................................... 9 Criteri di visita di un albero BST....................................................................................................................... 16 Visita di alberi binari (BST)........................................................................................................................... 17 Visita in pre-ordine e in post-ordine ....................................................................................................... 18 Visita in ampiezza .................................................................................................................................... 18 Algoritmi di visita ..................................................................................................................................... 20 Realizzazione di alberi binari di ricerca BST – criteri di visita. ..................................................................... 20 progetto Test1AlberoBST ........................................................................................................................ 20 Esercizi ................................................................................................................................................. 21 Diagramma UML delle classi del progetto Test1AlberoBST ................................................................ 26 Classi Generiche .............................................................................................................................................. 27 Perché generiche? ....................................................................................................................................... 28 Definizione di classi generiche .................................................................................................................... 28 Type Parameter Naming Conventions......................................................................................................... 30 Invoking and Instantiating a Generic Type .................................................................................................. 30 The Diamond ........................................................................................................................................... 30 Multiple Type Parameters ........................................................................................................................... 30 Parameterized Types ................................................................................................................................... 31 Generic Methods ......................................................................................................................................... 31 Bounded Type Parameters .......................................................................................................................... 32 Multiple Bounds ...................................................................................................................................... 33 Generic Methods and Bounded Type Parameters ...................................................................................... 33 Generics, Inheritance, and Subtypes ........................................................................................................... 34 Generic Classes and Subtyping .................................................................................................................... 35 Wildcards ..................................................................................................................................................... 35 Upper Bounded Wildcards ...................................................................................................................... 35 Unbounded Wildcards ............................................................................................................................. 36 Lower Bounded Wildcards....................................................................................................................... 37 Wildcards and Subtyping ......................................................................................................................... 37 Alberi Binari Generici ....................................................................................................................................... 38 Classe AlberoBST ver.2: Progetto Test2AlberoBST .................................................................................. 38 //Classe MioOggetto ........................................................................................................................... 38 //classe Nodo....................................................................................................................................... 39 //Classe AlberoBST .............................................................................................................................. 39 Verso un progetto migliore della classe AlberoBST .................................................................................... 42 Classe AlberoBST ver.3: progetto Test3AlberoBST.................................................................................. 42 //Classe Nodo ...................................................................................................................................... 42 //classe AlberoBST ............................................................................................................................... 43 //classe VoceDizionario ....................................................................................................................... 46 Diagramma UML del progetto Test3AlberoBST .................................................................................. 49 Classe AlberoBST ver.4: progetto Test4AlberoBST.................................................................................. 49 Progetto Dizionario: il benchmark definitivo .................................................................................................. 51 Package Dizionario ...................................................................................................................................... 52 Package tree ................................................................................................................................................ 52 Classe Dizionario .......................................................................................................................................... 53 ArrayList vs. AlberoBST vs. HashMap .......................................................................................................... 55 Tipologie di Alberi binari di ricerca .................................................................................................................. 60 Alberi nelle API di Java..................................................................................................................................... 60 TreeMap<K,V> ............................................................................................................................................. 60 TreeSet<E>................................................................................................................................................... 61 Premessa In queste note si introdurranno gli alberi (ADT tree) in generale e gli alberi binari di ricerca (BST) in dettaglio, sviluppando anche classi generiche. L’obiettivo di questa unità didattica è duplice: comprendere le caratteristiche e gli usi degli alberi come ADT e imparare a scrivere classi generiche. Alberi Un albero è una struttura dati (ADT) che può essere vista come un particolare grafo (aciclico), con le seguenti condizioni aggiuntive: https://en.wikipedia.org/wiki/Tree_(data_structure) https://it.wikipedia.org/wiki/Albero_(informatica) Ogni nodo deve avere al massimo un unico arco entrante, mentre dai diversi nodi possono uscire diversi numeri di archi uscenti. Si chiede infine che l'albero possegga un unico nodo privo di arco entrante: questo nodo viene detto radice (root) dell'albero. Un albero si compone di due tipi di sottostrutture fondamentali: il nodo, che in genere contiene informazioni, e l'arco che stabilisce un collegamento gerarchico fra due nodi: si parla allora di un nodo padre dal quale esce un arco orientato che lo collega ad un nodo figlio Solitamente ogni nodo porta con sé delle informazioni e molto spesso anche una chiave con cui è possibile identificarlo univocamente all'interno dell'albero. L'altezza o profondità dell'albero è il massimo delle lunghezze dei suoi cammini massimali, cammini che vanno dalla radice alle sue foglie. Perché si utilizzano gli alberi? Perché gli alberi sono utilizzati in alcune applicazioni informatiche? Per quale motivo non bastano le liste? Alberi binari Alberi binari di ricerca - BST L’albero binario è di ricerca se esiste una relazione di ordinamento tra i valori dei nodi (valori comparabili). In particolare, dato un nodo, il sottoalbero sinistro ha nodi i cui valori sono più piccoli di quello del nodo d’origine, mentre il sottoalbero destro ha nodi con valori più grandi. Un BST soddisfa le seguenti proprietà: - ogni nodo v contiene un elemento cui è associato una chiave presa da un dominio totalmente ordinato. - le chiavi nel sottoalbero sinistro di v sono minori della chiave di v - le chiavi nel sottoalbero destro di v sono maggiori della chiave di v Le operazioni tipiche che possono essere svolte sulle collezioni di dati sono search, insert e delete. Prestazioni di un albero binario di ricerca - BST BST vs Array vs LinkedList Su un array di oggetti si può effettuare un algoritmo di ricerca con complessità O(n). Infatti se l’array non è ordinato è possibile, nel caso peggiore, dover effettuare tutti i confronti sulle chiavi di ricerca di tutti gli oggetti. Su un array di oggetti ordinato è possibile effettuare un algoritmo di ricerca, detta ricerca dicotomica, con complessità O(log n). https://it.wikipedia.org/wiki/Ricerca_dicotomica public class BinarySearch { public int binarySearch (int[] a, int p, int r, int k) { int q, s = -1; if (p <= r) { q = (p+r)/2; if (k < a[q]) s = binarySearch(a, p, q-1, k); else if (k > a[q]) s = binarySearch(a, q+1, r, k); else if (k == a[q]) return q; } return s; } } Tuttavia gli algoritmi per gli inserimenti e le cancellazioni su un array ordinato hanno complessità O(n). Ad esempio, per inserire un elemento in un array ordinato, bisogna prima trovare la posizione in cui inserire l’elemento e poi eventualmente spostare gli elementi maggiori (minori) dell’elemento inserito. Su una lista concatenata (ADT LinkedList) è possibile effettuare un algoritmo di ricerca con complessità O(n), come nel caso dell’array non ordinato, tuttavia su una lista concatenata non ha senso effettuare la ricerca dicotomica dal momento che l’accesso a un elemento generico di una lista concatenata si ottiene attraversando la catena di puntatori e non in maniera diretta come in un array o in un ArrayList (accesso casuale). Gli alberi binari di ricerca (ADT BST) permettono di ottenere accessi, inserimenti e cancellazioni in tempi O(log n). Inoltre gli alberi binari BST sono già ordinati e permettono di effettuare una scansione ordinata dell’insieme di valori in O(n) (che è il risultato migliore in assoluto). BST vs hashtable (hashmap) http://www.geeksforgeeks.org/advantages-of-bst-over-hash-table/ http://cs.stackexchange.com/questions/270/hash-tables-versus-binary-trees Hash Table supports following operations in O(1) time (se non ci sono collisioni, altrimenti O(n)) . 1) Search 2) Insert 3) Delete The time complexity of above operations in a self-balancing Binary Search Tree (BST) (like Red-Black Tree, AVL Tree, Splay Tree, etc) is O(Logn). So Hash Table seems to beating BST in all common operations. When should we prefer BST over Hash Tables, what are advantages. Following are some important points in favor of BSTs. 1. We can get all keys in sorted order by just doing “Inorder Traversal” of BST. This is not a natural operation in Hash Tables and requires extra efforts. 2. Doing order statistics, finding closest lower and greater elements, doing range queries are easy to do with BSTs. Like sorting, these operations are not a natural operation with Hash Tables. 3. BSTs are easy to implement compared to hashing, we can easily implement our own customized BST. To implement Hashing, we generally rely on libraries provided by programming languages. 4. With BSTs, all operations are guaranteed to work in O(Logn) time. But with Hashing, O(1) is average time and some particular operations may be costly, especially when table resizing happens. Realizzazione di alberi binari di ricerca BST Un albero BST può essere realizzato mediante la seguente architettura di classi – progetto Test1AlberoBST: Dove: //classe MioOggetto public class MioOggetto implements Comparable<MioOggetto> { String value; public MioOggetto() { value = ""; } public MioOggetto(String value) { this.value = value; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public int compareTo(MioOggetto otherObject) { return this.value.compareTo(otherObject.getValue()); } @Override public String toString(){ return value.toString(); } } //classe Nodo public class Nodo { private MioOggetto value; private Nodo left; // figlio sinistro private Nodo right;// figlio destro public Nodo(MioOggetto valore) { value = valore; left = null; right = null; } public MioOggetto getValue() { return value; } public void setLeftChild(Nodo child) { left = child; } public void setRightChild(Nodo child) { right = child; } public Nodo getLeftChild() { return left; } public Nodo getRightChild() { return right; } } //Classe TreeException public class TreeException extends Exception { TreeException() { } TreeException(String message) { super(message); } } //Classe AlberoBST import java.util.ArrayList; import java.util.List; enum VisitCriteria { PREORDER, POSTORDER, INORDER }; public class AlberoBST { private Nodo root; public AlberoBST() { root = null; } //Classe MainTest import java.util.List; /** * Programma che testa la classe AlberoBST * @author Gennaro */ public class MainTest { public static void main(String[] args) { AlberoBST albero2 = new AlberoBST(); /* MioOggetto MioOggetto MioOggetto MioOggetto MioOggetto oo1 oo2 oo3 oo4 oo5 = = = = = new new new new new MioOggetto("ciao"); MioOggetto("alto"); MioOggetto("dato"); MioOggetto("basso"); MioOggetto("brutto"); /* albero2.inserisciValore(oo1); albero2.inserisciValore(oo2); albero2.inserisciValore(oo3); albero2.inserisciValore(oo4); albero2.inserisciValore(oo5); */ try { //fai qualcosa con l’albero… } catch (TreeException e) { e.printStackTrace(); } } } /** * * @param valore valore da inserire * @return true se il valore è inserito, false altrimenti */ public boolean inserisciValore(MioOggetto valore) { if (root != null) return insert(valore, root); else { root = new Nodo(valore); return true; } } /** * Metodo ricorsivo per l'inserimento di un valore in un nodo * @param valore valore da inserire * @param nodoCorrente nodo corrente * @return true, se il valore da inserire è inserito correttamente, false altrimenti. */ protected boolean insert(MioOggetto valore, Nodo nodoCorrente) { if (valore.compareTo(nodoCorrente.getValue()) == 0) // già presente return false; if (valore.compareTo(nodoCorrente.getValue()) < 0) { if (nodoCorrente.getLeftChild() == null) { nodoCorrente.setLeftChild(new Nodo(valore)); return true; } else return insert(valore, nodoCorrente.getLeftChild()); } else {// caso valore.compareTo(nodoCorrente.getValue())>0 if (nodoCorrente.getRightChild() == null) { nodoCorrente.setRightChild(new Nodo(valore)); return true; } else return insert(valore, nodoCorrente.getRightChild()); } } /** * Metodo per la ricerca di un valore nell'albero BST * @param valore valore da ricercare * @return true, se il valore da ricercare è presente */ public boolean ricercaValore(MioOggetto valore) { return search(valore, root); } /** * Metodo ricorsivo per la ricerca di un valore in un nodo * @param valore valore da ricercare * @param nodoCorrente nodo corrente * @return true, se il valore è stato trovato, false altrimenti */ protected boolean search(MioOggetto valore, Nodo nodoCorrente) { if (nodoCorrente == null) { return false; } if (valore.compareTo(nodoCorrente.getValue()) < 0) return search(valore, nodoCorrente.getLeftChild()); else { if (valore.compareTo(nodoCorrente.getValue()) > 0) return search(valore, nodoCorrente.getRightChild()); else return true; } } //La cancellazione di un elemento da un albero BST è l’operazione più complessa! /** * Metodo per cancellare un valore dall'albero * @param valore valore da cancellare * @throws TreeException se l'elemento non è presente */ public void cancellaValore(MioOggetto valore) throws TreeException { if (valore.compareTo(root.getValue()) == 0) { //devo cancellare il nodo radice //appendo il sottoalbero sinistro come elemento più piccolo del sottoalbero destro tramite il metodo insertTree(treeRoot, addedTreeRoot) insertTree(root.getRightChild(), root.getLeftChild()); root = root.getRightChild();// elimina il padre } else { if (valore.compareTo(root.getValue()) < 0) //elimino nel sottoalbero sinistro delete(valore, root, root.getLeftChild()); else //elimino nel sottoalbero destro delete(valore, root, root.getRightChild()); } } /** * Metodo ricorsivo per la cancellazione di un elemento dall'albero * @param valore valore da rimuovere * @param parent nodo padre * @param nodoCorrente nodo corrente * @throws TreeException se il valore non è presente */ protected void delete(MioOggetto valore, Nodo parent, Nodo nodoCorrente) throws TreeException { if (nodoCorrente == null) throw new TreeException("Valore inesistente"); if (valore.compareTo(nodoCorrente.getValue()) < 0) delete(valore, nodoCorrente, nodoCorrente.getLeftChild()); else { if (valore.compareTo(nodoCorrente.getValue()) > 0) delete(valore, nodoCorrente, nodoCorrente.getRightChild()); else { // nodoCorrente è il nodo che voglio cancellare Nodo temp; if (nodoCorrente.getRightChild() == null) temp = nodoCorrente.getLeftChild(); // caso con nodoCorrente senza figlio destro else { // caso con nodocorrente che ha il figlio destro parent nodocorrente 3 1 6 8 8 10 10 6 14 2 13 4 14 4 1 7 13 7 2 insertTree(nodoCorrente.getRightChild(), nodoCorrente.getLeftChild()); temp = nodoCorrente.getRightChild(); insertTree(nodoCorrente.getRightChild(), nodoCorrente.getLeftChild()); } if (parent.getLeftChild() == nodoCorrente) parent.setLeftChild(temp); else parent.setRightChild(temp); } } } /** * inserisce il sottoalbero addedTreeRoot come elemento più piccolo del * sottoalbero che ha radice in treeRoot se treeRoot è non nullo * * @param treeRoot * @param addedTreeRoot */ private void insertTree(Nodo treeRoot, Nodo addedTreeRoot) { Nodo parent = null; while (treeRoot != null) { parent = treeRoot; treeRoot = treeRoot.getLeftChild(); } if (parent != null) parent.setLeftChild(addedTreeRoot); } //altri metodi da implementare… }//fine classe Criteri di visita di un albero BST Visita di alberi binari (BST) Visita in ordine Visita in pre-ordine e in post-ordine Visita in ampiezza Algoritmi di visita Realizzazione di alberi binari di ricerca BST – criteri di visita. progetto Test1AlberoBST Partendo dal seguente codice di esempio: /** * * @throws TreeException */ public void stampaInOrdine() throws TreeException { if (root == null) throw new TreeException("Albero vuoto"); else inOrderPrint(root); System.out.println(); } protected void inOrderPrint(Nodo nodo) { if (nodo != null) { inOrderPrint(nodo.getLeftChild()); System.out.print(nodo.getValue() + " "); inOrderPrint(nodo.getRightChild()); } } @Override public String toString() { return inOrderToString(root); } protected String inOrderToString(Nodo n) { String left, center, right; if (n != null) { left = inOrderToString(n.getLeftChild()); center = n.getValue() + " "; right = inOrderToString(n.getRightChild()); return left + center + right; } // else non serve return ""; } public void stampa() throws TreeException { if (root == null) throw new TreeException("Albero vuoto"); else System.out.println(inOrderToString(root)); } Esercizi Esercizio 1: Scrivere i seguenti metodi per la classe AlberoBST: public void stampa(VisitCriteria criteria) throws TreeException; protected String postOrderToString(Nodo n); protected String preOrderToString(Nodo n) DEF: Livello di un albero binario. Si definisce livello di un nodo in un albero binario il numero di rami da attraversare dalla radice fino al nodo. Ad esempio la radice ha livello 0, i due sottoalberi della radice hanno livello 1 e così via. DEF: Livello massimo di un albero binario. Si definisce livello massimo di un albero binario il massimo numero di rami da attraversare, partendo dalla radice per raggiungere un nodo. Il livello massimo di un albero binario identifica il nodo (foglia) più distante dalla radice. DEF: Livello minimo di un albero binario. Si definisce livello minimo di un albero binario il minimo numero di rami da attraversare partendo dalla radice per raggiungere una foglia dell’albero. Il livello minimo di un albero identifica la foglia più vicina alla radice (e distinta da essa). DEF: albero binario bilanciato. Un albero è bilanciato quando la differenza tra il ramo più corto e quello più lungo è al più uno. Esercizio 2: prendendo spunto dal seguente metodo public int getLivelloMassimo() throws TreeException { if (root == null) throw new TreeException("Albero vuoto"); else return postOrderLevelCountMax(root); } /** * * @param n * */ private int postOrderLevelCountMax(Nodo n) { int level1 =0; int level2 =0; if (n != null) { if (n.getLeftChild() != null) level1 = postOrderLevelCountMax(n.getLeftChild())+1; if (n.getRightChild() != null) level2 = postOrderLevelCountMax(n.getRightChild())+1; return level1 > level2 ? level1 : level2;// prendo il massimo } else return 0; } Esercizio 3: Scrivere i seguenti metodi: public int getLivelloMinimo() throws TreeException; public boolean isBalanced() throws TreeException; Esercizio 4: Scrivere il metodo che conta il numero di nodi di un albero: public int getNumeroNodi() { return getPostOrderNodeCount(root); } //come si scrive il metodo getPostOrderNodeCount ? Esercizio 5: prendendo spunto dal seguente esempio /** * * @return una Vista di tipo ArrayList degli oggetti dell'albero visitati in ordine * @throws TreeException */ public ArrayList<MioOggetto> asInOrderArrayListView() throws TreeException { if (root == null) throw new TreeException("Albero vuoto"); else { ArrayList<MioOggetto> list = new ArrayList<MioOggetto>( this.getNumeroNodi()); inOrderListFill(list, root); return list; } } /** * * @param l * lista da riempire con i dati (MioOggetto) dei nodi - l deve * essere non null * @param n * nodo radice dell'albero. Gli oggetti inseriti nella lista sono * gli stessi inseriti nell'albero, passiamo i riferimenti */ protected void inOrderListFill(List<MioOggetto> l, Nodo n) { if (n != null) { inOrderListFill(l, n.getLeftChild()); l.add(n.getValue()); inOrderListFill(l, n.getRightChild()); } } Scrivere il metodo: /** * * @param criteria * @return una Vista di tipo ArrayList degli oggetti dell'albero visitati secondo il criterio di visita specificato * @throws TreeException */ public ArrayList<MioOggetto> asArrayListView(VisitCriteria criteria) throws TreeException; Esercizio 6: completare il metodo che bilancia un albero binario /** * Bilancia l'albero. * @throws TreeException */ public void balance() throws TreeException { if (root == null) throw new TreeException("Albero vuoto"); else { //creo la lista con i gli oggetti (MioOggetto) in ordine ArrayList<MioOggetto> list = asArrayListView(VisitCriteria.INORDER); //creo un nuovo albero bilanciato a partire dalla lista ordinata //dell'albero restituito prendo il nodo radice e lo passo a root root = buildBalancedTreeFromOrderedList(list).root; } } /** * Metodo che restituisce un nuovo albero binario di ricerca costruito con gli oggetti presenti in una lista ordinata * * @param orderedList - deve essere non nullo * @return */ protected AlberoBST buildBalancedTreeFromOrderedList(List<MioOggetto> orderedList){ AlberoBST nuovoAlbero = new AlberoBST(); //creo un nuovo albero bilanciato a partire da list buildBalancedTreeFromOrderedList(orderedList,nuovoAlbero, 0,orderedList.size()-1); return nuovoAlbero; } Scrivere il metodo: /** * Metodo ricorsivo che crea un albero binario di ricerca a partire da una lista ordinata di oggetti (MioOggetto) * * @param orderedList - deve essere non null * @param albero - deve essere non null * @param left - indice minimo della lista * @param right - indice massimo della lista */ private void buildBalancedTreeFromOrderedList(List<MioOggetto> orderedList, AlberoBST albero, int left, int right); //suggerimento: sfruttare il fato che la lista è ordinata, selezionando di volta in volta l’elemento mediano della lista e inserirlo nell’albero con il metodo “inserisciValore”. Esercizio 7: Scrivere una versione efficiente del metodo che crea un albero binario di ricerca bilanciato. /** * Bilancia l'albero, usando un algoritmo efficiente di costruzione * dell'albero bilanciato. A partire dall'albero di input si crea una lista * ordinata facendo una visita in ordine dell'albero binario di ricerca. Con * la lista ordinata così ottenuta si crea un nuovo albero binario di * ricerca con un procedimento ricorsivo che prende l'elemento mediano della * lista ordinata e lo inserisce nel nodo radice; gli elementi più piccoli * dell'elemento mediano sono messi nel sottoalbero sinistro; gli elementi * più grandi dell'elemento mediano sono messi nel sottoalbero destro, e * così via fino a quando la sotto-lista non è vuota. In questo algoritmo non * è utilizzato il metodo inserisciValore che prevede di effettuare dei * confronti sugli elementi dell'albero per inserire un nuovo elemento, ma * la costruzione dell'albero è ottenuta direttamente a mano a mano che si * scorre ricorsivamente la lista ordinata. * * @throws TreeException */ public void efficientBalance() throws TreeException { if (root == null) { throw new TreeException("Albero vuoto"); } else { // creo la lista con i gli oggetti in ordine ArrayList<MioOggetto> list = asArrayListView(VisitCriteria.INORDER); // dell'albero restituito prendo il nodo radice e lo passo a root root = efficientBuildBalancedTreeFromSortedList(list).root; } } /** * Metodo che restituisce un nuovo albero binario di ricerca costruito con * gli oggetti presenti in una lista ordinata * * @param sortedList - deve essere non nullo. La lista passata come argomento può essere modificata: se nella lista ci sono duplicati, questi sono eliminati. * @return un albero binario di ricerca bilanciato con gli elementi della lista */ protected AlberoBST efficientBuildBalancedTreeFromSortedList(List<MioOggetto> sortedList) { AlberoBST nuovoAlbero = new AlberoBST(); // elimino elementi duplicati nella lista deleteDuplicateFromSortedList(sortedList); // creo un nuovo albero bilanciato a partire da list nuovoAlbero.root = efficientBuildBalancedTreeFromSortedList(sortedList, 0, sortedList.size() - 1); return nuovoAlbero; } Scrivere i metodi: /** * elimina elementi duplicati dalla lista ordinata; la lista in input deve * essere ordinata * * @param theSortedList */ private void deleteDuplicateFromSortedList(List<MioOggetto> theSortedList); e /* * Metodo ricorsivo che crea un albero binario di ricerca a partire da una * lista ordinata di oggetti * * @param sortedList - deve essere non null e ordinata. Inoltre questa * versione richiede che nella lista ordinata non ci siano duplicati * * @param left - indice minimo della lista * * @param right - indice massimo della lista */ private Nodo efficientBuildBalancedTreeFromSortedList(List<MioOggetto> sortedList, int left, int right); Esercizio 8: [Risolto] scrivere altri due costruttori per la classe AlberoBST. Il primo riceve in input una lista di MioOggetto, non necessariamente ordinata, e a partire da questo crea un albero binario di ricerca usando il metodo inserisciValore. Il secondo riceve in input due parametri: il primo è una lista di MioOggetto, non necessariamente ordinata, e il secondo parametro è una variabile booleana che se true forza il costruttore a creare un albero bilanciato. public AlberoBST(List<MioOggetto> list) { root = null; // mischio gli elementi della lista per migliorare il bilanciamento // dell'albero Collections.shuffle(list); for (MioOggetto elem : list) { inserisciValore(elem); } } public AlberoBST(List<MioOggetto> list, boolean forceBalance) { if (forceBalance) { // ordino la lista Collections.sort(list); // creo un albero bilanciato root =efficientBuildBalancedTreeFromSortedList(list).root; } else // creo un albero, ma non necessariamente bilanciato { root = new AlberoBST(list).root; } } Si noti che nel costruttore che riceve in input una lista di valori si effettua una randomizzazione dell’input prima di procedere alla costruzione dell’albero, mediante il metodo Collections.shuffle(list), per evitare sbilanciamenti troppo elevati nel caso di liste parzialmente o totalmente ordinate. Esercizio 9 - Scrivere i metodi: /** * Inserisce (aggiunge) una lista di oggetti all'albero. L'albero non è necessariamente bilanciato * @param listaValori la lista di oggetti da aggiungere all'albero */ public void inserisciListaValori(List<MioOggetto> listaValori); /** * Inserisce (aggiunge) una lista di oggetti all'albero. * @param listaValori lista di oggetti da aggiungere all'albero * @param forceBalance se true l'albero risultante è bilanciato, anche se l'albero di partenza non lo era */ public void inserisciListaValori(List<MioOggetto> listaValori, boolean forceBalance); Esercizio 10 – Scrivere il diagramma delle classi del progetto Test1AlberoBST (usando IntelliJ IDEA) e controllare che sia come il seguente. Diagramma UML delle classi del progetto Test1AlberoBST Classi Generiche La stragrande maggioranza delle cose riportate in questi appunti relativamente alle classi generiche sono prese dal tutorial: https://docs.oracle.com/javase/tutorial/java/generics/index.html Perché generiche? Una classe “generica” è uno strumento che permette la definizione di un tipo parametrizzato, che viene esplicitato in fase di compilazione secondo le necessità. Vantaggi: Evita il casting da Object. I.e., invece di String title = ((String) words.get(i)).toUppercase(); utilizzeremo String title = words.get(i).toUppercase(); Fornisce una migliore gestione del type checking durante la compilazione Permette di creare algoritmi generici https://docs.oracle.com/javase/tutorial/java/generics/why.html Code that uses generics has many benefits over non-generic code: Stronger type checks at compile time. A Java compiler applies strong type checking to generic code and issues errors if the code violates type safety. Fixing compile-time errors is easier than fixing runtime errors, which can be difficult to find. Elimination of casts. The following code snippet without generics requires casting: List list = new ArrayList(); list.add("hello"); String s = (String) list.get(0); When re-written to use generics, the code does not require casting: List<String> list = new ArrayList<String>(); list.add("hello"); String s = list.get(0); // no cast Enabling programmers to implement generic algorithms. By using generics, programmers can implement generic algorithms that work on collections of different types, which can be customized, and are type safe and easier to read. Definizione di classi generiche A generic type is a generic class or interface that is parameterized over types. The following Box class will be modified to demonstrate the concept. Begin by examining a non-generic Box class that operates on objects of any type. It needs only to provide two methods: set, which adds an object to the box, and get, which retrieves it: public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; } } Since its methods accept or return an Object, you are free to pass in whatever you want, provided that it is not one of the primitive types. There is no way to verify, at compile time, how the class is used. One part of the code may place an Integer in the box and expect to get Integers out of it, while another part of the code may mistakenly pass in a String, resulting in a runtime error. A generic class is defined with the following format: class name<T1, T2, ..., Tn> { /* ... */ } The type parameter section, delimited by angle brackets (<>), follows the class name. It specifies the type parameters (also called type variables)T1, T2, ..., and Tn. To update the Box class to use generics, you create a generic type declaration by changing the code "public class Box" to "public class Box<T>". This introduces the type variable, T, that can be used anywhere inside the class. With this change, the Box class becomes: /** * Generic version of the Box class. * @param <T> the type of the value being boxed */ public class Box<T> { // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } } As you can see, all occurrences of Object are replaced by T. A type variable can be any non-primitive type you specify: any class type, any interface type, any array type, or even another type variable. This same technique can be applied to create generic interfaces. Consider that in case you provide a non-primitive type as actual parameter to a method which expects a nonprimitive type Java compiler performs auto-boxing conversion: https://docs.oracle.com/javase/tutorial/java/data/autoboxing.html Autoboxing is the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes. The Java compiler applies autoboxing when a primitive value is: Passed as a parameter to a method that expects an object of the corresponding wrapper class. Assigned to a variable of the corresponding wrapper class. Consider the following method: public static int sumEven(List<Integer> li) { int sum = 0; for (Integer i: li) if (i % 2 == 0) sum += i; return sum; } Converting an object of a wrapper type (Integer) to its corresponding primitive (int) value is called unboxing. The Java compiler applies unboxing when an object of a wrapper class is: Passed as a parameter to a method that expects a value of the corresponding primitive type. Assigned to a variable of the corresponding primitive type. Type Parameter Naming Conventions By convention, type parameter names are single, uppercase letters. This stands in sharp contrast to the variable naming conventions that you already know about, and with good reason: Without this convention, it would be difficult to tell the difference between a type variable and an ordinary class or interface name. The most commonly used type parameter names are: E - Element (used extensively by the Java Collections Framework) K - Key N - Number T - Type V - Value S,U,V etc. - 2nd, 3rd, 4th types You'll see these names used throughout the Java SE API and the rest of this lesson. Invoking and Instantiating a Generic Type To reference the generic Box class from within your code, you must perform a generic type invocation, which replaces T with some concrete value, such as Integer: Box<Integer> integerBox; You can think of a generic type invocation as being similar to an ordinary method invocation, but instead of passing an argument to a method, you are passing a type argument — Integer in this case — to the Box class itself. The Diamond In Java SE 7 and later, you can replace the type arguments required to invoke the constructor of a generic class with an empty set of type arguments (<>) as long as the compiler can determine, or infer, the type arguments from the context. This pair of angle brackets, <>, is informally called the diamond. For example, you can create an instance of Box<Integer> with the following statement: Box<Integer> integerBox = new Box<>(); For more information on diamond notation and type inference, see Type Inference. Multiple Type Parameters As mentioned previously, a generic class can have multiple type parameters. For example, the generic OrderedPair class, which implements the generic Pair interface: public interface Pair<K, V> { public K getKey(); public V getValue(); } public class OrderedPair<K, V> implements Pair<K, V> { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } public K getKey() { return key; } public V getValue() { return value; } } The following statements create two instantiations of the OrderedPair class: Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8); Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world"); The code, new OrderedPair<String, Integer>, instantiates K as a String and V as an Integer. Therefore, the parameter types of OrderedPair's constructor are String and Integer, respectively. Due to autoboxing, it is valid to pass a String and an int to the class. As mentioned in The Diamond, because a Java compiler can infer the K and V types from the declaration OrderedPair<String, Integer>, these statements can be shortened using diamond notation: OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8); OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world"); To create a generic interface, follow the same conventions as for creating a generic class. Parameterized Types You can also substitute a type parameter (i.e., K or V) with a parameterized type (i.e., List<String>). For example, using the OrderedPair<K, V>example: OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...)); Generic Methods Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter's scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors. The syntax for a generic method includes a type parameter, inside angle brackets, and appears before the method's return type. For static generic methods, the type parameter section must appear before the method's return type. The Util class includes a generic method, compare, which compares two Pair objects: public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public public public public void setKey(K key) { this.key = key; } void setValue(V value) { this.value = value; } K getKey() { return key; } V getValue() { return value; } } The complete syntax for invoking this method would be: Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.<Integer, String>compare(p1, p2); The type has been explicitly provided, as shown in bold. Generally, this can be left out and the compiler will infer the type that is needed: Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.compare(p1, p2); This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets. This topic is further discussed in the following section, Type Inference. Bounded Type Parameters There may be times when you want to restrict the types that can be used as type arguments in a parameterized type. For example, a method that operates on numbers might only want to accept instances of Number or its subclasses. This is what bounded type parameters are for. To declare a bounded type parameter, list the type parameter's name, followed by the extends keyword, followed by its upper bound, which in this example is Number. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces). public class Box<T> { private T t; public void set(T t) { this.t = t; } public T get() { return t; } public <U extends Number> void inspect(U u){ System.out.println("T: " + t.getClass().getName()); System.out.println("U: " + u.getClass().getName()); } public static void main(String[] args) { Box<Integer> integerBox = new Box<Integer>(); integerBox.set(new Integer(10)); integerBox.inspect("some text"); // error: this is still String! } } By modifying our generic method to include this bounded type parameter, compilation will now fail, since our invocation of inspect still includes aString: Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot be applied to (java.lang.String) integerBox.inspect("10"); ^ 1 error In addition to limiting the types you can use to instantiate a generic type, bounded type parameters allow you to invoke methods defined in the bounds: public class NaturalNumber<T extends Integer> { private T n; public NaturalNumber(T n) { this.n = n; } public boolean isEven() { return n.intValue() % 2 == 0; } // ... } The isEven method invokes the intValue method defined in the Integer class through n. Multiple Bounds The preceding example illustrates the use of a type parameter with a single bound, but a type parameter can have multiple bounds: <T extends B1 & B2 & B3> A type variable with multiple bounds is a subtype of all the types listed in the bound. If one of the bounds is a class, it must be specified first. For example: Class A { /* ... */ } interface B { /* ... */ } interface C { /* ... */ } class D <T extends A & B & C> { /* ... */ } If bound A is not specified first, you get a compile-time error: class D <T extends B & A & C> { /* ... */ } // compile-time error Generic Methods and Bounded Type Parameters Bounded type parameters are key to the implementation of generic algorithms. Consider the following method that counts the number of elements in an array T[] that are greater than a specified element elem. public static <T> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e > elem) // compiler error ++count; return count; } The implementation of the method is straightforward, but it does not compile because the greater than operator (>) applies only to primitive types such as short, int, double, long, float, byte, and char. You cannot use the > operator to compare objects. To fix the problem, use a type parameter bounded by the Comparable<T> interface: public interface Comparable<T> { public int compareTo(T o); } The resulting code will be: public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) > 0) ++count; return count; } Generics, Inheritance, and Subtypes As you already know, it is possible to assign an object of one type to an object of another type provided that the types are compatible. For example, you can assign an Integer to an Object, since Object is one of Integer's supertypes: Object someObject = new Object(); Integer someInteger = new Integer(10); someObject = someInteger; // OK In object-oriented terminology, this is called an "is a" relationship. Since an Integer is a kind of Object, the assignment is allowed. But Integer is also a kind of Number, so the following code is valid as well: public void someMethod(Number n) { /* ... */ } someMethod(new Integer(10)); someMethod(new Double(10.1)); // OK // OK The same is also true with generics. You can perform a generic type invocation, passing Number as its type argument, and any subsequent invocation of add will be allowed if the argument is compatible with Number: Box<Number> box = new Box<Number>(); box.add(new Integer(10)); // OK box.add(new Double(10.1)); // OK Now consider the following method: public void boxTest(Box<Number> n) { /* ... */ } What type of argument does it accept? By looking at its signature, you can see that it accepts a single argument whose type is Box<Number>. But what does that mean? Are you allowed to pass in Box<Integer> or Box<Double>, as you might expect? The answer is "no", because Box<Integer> and Box<Double> are not subtypes of Box<Number>. This is a common misunderstanding when it comes to programming with generics, but it is an important concept to learn. Box<Integer> is not a subtype of Box<Number> even though Integer is a subtype of Number. Note: Given two concrete types A and B (for example, Number and Integer), MyClass<A> has no relationship to MyClass<B>, regardless of whether or not A and B are related. The common parent of MyClass<A> and MyClass<B> is Object. Generic Classes and Subtyping You can subtype a generic class or interface by extending or implementing it. The relationship between the type parameters of one class or interface and the type parameters of another are determined by the extends and implements clauses. the Collections classes as an example, ArrayList<E> implements List<E>, and List<E> extends Collection<E>. So ArrayList<String> is a subtype of List<String>, which is a subtype of Collection<String>. So long as you do not vary the type argument, the subtyping relationship is preserved between the types. Using A sample Collections hierarchy Wildcards In generic code, the question mark (?), called the wildcard, represents an unknown type. The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type (though it is better programming practice to be more specific). The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype. Upper Bounded Wildcards You can use an upper bounded wildcard to relax the restrictions on a variable. For example, say you want to write a method that works on List<Integer>, List<Double>, and List<Number>; you can achieve this by using an upper bounded wildcard. To declare an upper-bounded wildcard, use the wildcard character ('?'), followed by the extends keyword, followed by its upper bound. Note that, in this context, extends is used in a general sense to mean either "extends" (as in classes) or "implements" (as in interfaces). To write the method that works on lists of Number and the subtypes of Number, such as Integer, Double, and Float, you would specify List<? extends Number>. The term List<Number> is more restrictive than List<? extends Number> because the former matches a list of type Number only, whereas the latter matches a list of type Number or any of its subclasses. Consider the following process method: public static void process(List<? extends Foo> list) { /* ... */ } The upper bounded wildcard, <? extends Foo>, where Foo is any type, matches Foo and any subtype of Foo. The process method can access the list elements as type Foo: public static void process(List<? extends Foo> list) { for (Foo elem : list) { // ... } } In the foreach clause, the elem variable iterates over each element in the list. Any method defined in the Foo class can now be used on elem. The sumOfList method returns the sum of the numbers in a list: public static double sumOfList(List<? extends Number> list) { double s = 0.0; for (Number n : list) s += n.doubleValue(); return s; } Unbounded Wildcards The unbounded wildcard type is specified using the wildcard character (?), for example, List<?>. This is called a list of unknown type. There are two scenarios where an unbounded wildcard is a useful approach: If you are writing a method that can be implemented using functionality provided in the Object class. When the code is using methods in the generic class that don't depend on the type parameter. For example, List.size or List.clear. In fact,Class<?> is so often used because most of the methods in Class<T> do not depend on T. Consider the following method, printList: public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); } The goal of printList is to print a list of any type, but it fails to achieve that goal — it prints only a list of Object instances; it cannot printList<Integer>, List<String>, List<Double>, and so on, because they are not subtypes of List<Object>. To write a generic printList method, useList<?>: public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + " "); System.out.println(); } Lower Bounded Wildcards The Upper Bounded Wildcards section shows that an upper bounded wildcard restricts the unknown type to be a specific type or a subtype of that type and is represented using the extends keyword. In a similar way, a lower bounded wildcard restricts the unknown type to be a specific type or a super type of that type. A lower bounded wildcard is expressed using the wildcard character ('?'), following by the super keyword, followed by its lower bound: <? super A>. Note: You can specify an upper bound for a wildcard, or you can specify a lower bound, but you cannot specify both. Say you want to write a method that puts Integer objects into a list. To maximize flexibility, you would like the method to work on List<Integer>, List<Number>, and List<Object> — anything that can hold Integer values. To write the method that works on lists of Integer and the supertypes of Integer, such as Integer, Number, and Object, you would specify List<? super Integer>. The term List<Integer> is more restrictive than List<? super Integer> because the former matches a list of type Integer only, whereas the latter matches a list of any type that is a supertype of Integer. The following code adds the numbers 1 through 10 to the end of a list: public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } } Wildcards and Subtyping As described in Generics, Inheritance, and Subtypes, generic classes or interfaces are not related merely because there is a relationship between their types. However, you can use wildcards to create a relationship between generic classes or interfaces. Given the following two regular (non-generic) classes: class A { /* ... */ } class B extends A { /* ... */ } It would be reasonable to write the following code: B b = new B(); A a = b; This example shows that inheritance of regular classes follows this rule of subtyping: class B is a subtype of class A if B extends A. This rule does not apply to generic types: List<B> lb = new ArrayList<>(); List<A> la = lb; // compile-time error Given that Integer is a subtype of Number, what is the relationship between List<Integer> and List<Number>? The common parent is List<?>. Although Integer is a subtype of Number, List<Integer> is not a subtype of List<Number> and, in fact, these two types are not related. The common parent of List<Number> and List<Integer> is List<?>. In order to create a relationship between these classes so that the code can access Number's methods through List<Integer>'s elements, use an upper bounded wildcard: List<? extends Integer> intList = new ArrayList<>(); List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number> Alberi Binari Generici Classe AlberoBST ver.2: Progetto Test2AlberoBST Creare un nuovo progetto con una versione generica delle classi MioOggetto, Nodo e Albero. //Classe MioOggetto // http://stackoverflow.com/questions/12234219/implementing-compareto-withgenerics-and-collection public class MioOggetto<T extends Comparable<T>> implements Comparable<MioOggetto<T>> { T value; public MioOggetto(T value) { this.value = value; } public T getValue() { return value; } public void setValue(T value) { this.value = value; } @Override public int compareTo(MioOggetto<T> elem) { return this.value.compareTo(elem.getValue()); } @Override public String toString(){ return value.toString(); } } //classe Nodo public class Nodo<T extends Comparable<T>> { private MioOggetto<T> value; private Nodo<T> left; // figlio sinistro private Nodo<T> right;// figlio destro public Nodo(MioOggetto<T> valore) { value = valore; left = null; right = null; } public MioOggetto<T> getValue() { return value; } public void setLeftChild(Nodo<T> child) { left = child; } public void setRightChild(Nodo<T> child) { right = child; } public Nodo<T> getLeftChild() { return left; } public Nodo<T> getRightChild() { return right; } } //Classe AlberoBST import java.util.ArrayList; import java.util.List; public class AlberoBST<T extends Comparable<T>> { private Nodo<T> root; public AlberoBST() { root = null; } public boolean inserisciValore(MioOggetto<T> valore) { if (root != null) return insert(valore, root); else { root = new Nodo<>(valore); return true; } } protected boolean insert(MioOggetto<T> valore, Nodo<T> nodoCorrente) { if (valore.compareTo(nodoCorrente.getValue()) == 0) // già presente return false; if (valore.compareTo(nodoCorrente.getValue()) < 0) { if (nodoCorrente.getLeftChild() == null) { nodoCorrente.setLeftChild(new Nodo<>(valore)); return true; } else return insert(valore, nodoCorrente.getLeftChild()); } else {// caso valore.compareTo(nodoCorrente.getValue())>0 if (nodoCorrente.getRightChild() == null) { nodoCorrente.setRightChild(new Nodo<>(valore)); return true; } else return insert(valore, nodoCorrente.getRightChild()); } } public boolean ricercaValore(MioOggetto<T> valore) { return search(valore, root); } protected boolean search(MioOggetto<T> valore, Nodo<T> nodoCorrente) { if (nodoCorrente == null) { return false; } if (valore.compareTo(nodoCorrente.getValue()) < 0) return search(valore, nodoCorrente.getLeftChild()); else { if (valore.compareTo(nodoCorrente.getValue()) > 0) return search(valore, nodoCorrente.getRightChild()); else return true; } } public void cancellaValore(MioOggetto<T> valore) throws TreeException { if (valore.compareTo(root.getValue()) == 0) { insertTree(root.getRightChild(), root.getLeftChild()); root = root.getRightChild();// elimina il padre } else { if (valore.compareTo(root.getValue()) < 0) delete(valore, root, root.getLeftChild()); else delete(valore, root, root.getRightChild()); } } protected void delete(MioOggetto<T> valore, Nodo<T> parent, Nodo<T> nodoCorrente) throws TreeException { if (nodoCorrente == null) throw new TreeException("Valore inesistente"); if (valore.compareTo(nodoCorrente.getValue()) < 0) delete(valore, nodoCorrente, nodoCorrente.getLeftChild()); else { if (valore.compareTo(nodoCorrente.getValue()) > 0) delete(valore, nodoCorrente, nodoCorrente.getRightChild()); else { // nodoCorrente è il nodo che voglio cancellare Nodo<T> temp; if (nodoCorrente.getRightChild() == null) temp = nodoCorrente.getLeftChild(); else { temp = nodoCorrente.getRightChild(); insertTree(nodoCorrente.getRightChild(), nodoCorrente.getLeftChild()); } if (parent.getLeftChild() == nodoCorrente) parent.setLeftChild(temp); else parent.setRightChild(temp); } } } /** * inserisce il sottoalbero addedTreeRoot come elemento più piccolo del * sottoalbero che ha radice in treeRoot se treeRoot è non nullo * * @param treeRoot * @param addedTreeRoot */ private void insertTree(Nodo<T> treeRoot, Nodo<T> addedTreeRoot) { Nodo<T> parent = null; while (treeRoot != null) { parent = treeRoot; treeRoot = treeRoot.getLeftChild(); } if (parent != null) parent.setLeftChild(addedTreeRoot); } // e gli altri metodi da adattare… } Esercizio 8: Adattare tutti i metodi del progetto AlberoBST in modo da supportare il tipo generico T. Nel main di prova si dovrà testare il corretto funzionamento della classe con diversi tipi base: AlberoBST<String> albero2 = new AlberoBST<>(); MioOggetto<String> oo1 = new MioOggetto<>("ciao"); MioOggetto<String> oo2 = new MioOggetto<>("alto"); MioOggetto<String> oo3 = new MioOggetto<>("dato"); MioOggetto<String> oo4 = new MioOggetto<>("basso"); MioOggetto<String> oo5 = new MioOggetto<>("brutto"); albero2.inserisciValore(oo1); albero2.inserisciValore(oo2); albero2.inserisciValore(oo3); albero2.inserisciValore(oo4); albero2.inserisciValore(oo5); //serie di test sull’albero //prova con interi AlberoBST<Integer> albero3 = new AlberoBST<>(); MioOggetto<Integer> io1 = new MioOggetto<>(10); MioOggetto<Integer> io2 = new MioOggetto<>(5); MioOggetto<Integer> io3 = new MioOggetto<>(3); MioOggetto<Integer> io4 = new MioOggetto<>(30); MioOggetto<Integer> io5 = new MioOggetto<>(8); albero3.inserisciValore(io1); albero3.inserisciValore(io2); albero3.inserisciValore(io3); albero3.inserisciValore(io4); albero3.inserisciValore(io5); //serie di test sull’albero Verso un progetto migliore della classe AlberoBST Con le classi Generiche, non ha molto senso usare la classe MioOggetto che era stata introdotta come classe wrapper di un tipo base. A questo punto si può scrivere la classe AlberoBST assumendo che il tipo Nodo sia costituito da un tipo base T… Classe AlberoBST ver.3: progetto Test3AlberoBST Obiettivi: classe generica, serializzabile, senza la classe MioOggetto. Creare anche una classe VoceDizionario che permetta di rappresentare le voci di un dizionario di lingua (ad esempio di Inglese - Italiano, come riportato nel codice d’esempio). //Classe Nodo import java.io.Serializable; // http://stackoverflow.com/questions/12234219/implementing-compareto-withgenerics-and-collection public class Nodo<T extends Serializable & Comparable<T>> implements Serializable{ /** * */ private static final long serialVersionUID = 657720013517038085L; private T value; private Nodo<T> left; // figlio sinistro private Nodo<T> right;// figlio destro public Nodo(T valore) { value = valore; left = null; right = null; } public T getValue() { return value; } public void setLeftChild(Nodo<T> child) { left = child; } public void setRightChild(Nodo<T> child) { right = child; } public Nodo<T> getLeftChild() { return left; } public Nodo<T> getRightChild() { return right; } } //classe AlberoBST import import import import import import import import import import java.io.File; java.io.FileInputStream; java.io.FileOutputStream; java.io.IOException; java.io.ObjectInputStream; java.io.ObjectOutputStream; java.io.Serializable; java.util.ArrayList; java.util.Collections; java.util.List; public class AlberoBST<T extends Serializable & Comparable<T>> implements Serializable { /** * */ private static final long serialVersionUID = -8908773846012112953L; private Nodo<T> root; public AlberoBST() { root = null; } public AlberoBST(List<T> lista) { root = null; // mischio gli elementi della lista per migliorare il bilanciamento // dell'albero Collections.shuffle(lista); for (T elem : lista) { inserisciValore(elem); } } public AlberoBST(List<T> list, boolean forceBalance) { if (forceBalance) { // ordino la lista Collections.sort(list); // creo un albero bilanciato root = buildBalancedTreeFromSortedList(list).root; } else // creo un albero, ma non necessariamente bilanciato { root = new AlberoBST<>(list).root; } } public boolean inserisciValore(T valore) { if (root != null) { return insert(valore, root); } else { root = new Nodo<>(valore); return true; } } protected boolean insert(T valore, Nodo<T> nodoCorrente) { if (valore.compareTo(nodoCorrente.getValue()) == 0) // già presente { return false; } if (valore.compareTo(nodoCorrente.getValue()) < 0) { if (nodoCorrente.getLeftChild() == null) { nodoCorrente.setLeftChild(new Nodo<>(valore)); return true; } else { return insert(valore, nodoCorrente.getLeftChild()); } } else {// caso valore.compareTo(nodoCorrente.getValue())>0 if (nodoCorrente.getRightChild() == null) { nodoCorrente.setRightChild(new Nodo<>(valore)); return true; } else { return insert(valore, nodoCorrente.getRightChild()); } } } public boolean ricercaValore(T valore) { return search(valore, root); } protected boolean search(T valore, Nodo<T> nodoCorrente) { if (nodoCorrente == null) { return false; } if (valore.compareTo(nodoCorrente.getValue()) < 0) { return search(valore, nodoCorrente.getLeftChild()); } else { if (valore.compareTo(nodoCorrente.getValue()) > 0) { return search(valore, nodoCorrente.getRightChild()); } else { return true; } } } public void cancellaValore(T valore) throws TreeException { if (valore.compareTo(root.getValue()) == 0) { insertTree(root.getRightChild(), root.getLeftChild()); root = root.getRightChild();// elimina il padre } else { if (valore.compareTo(root.getValue()) < 0) { delete(valore, root, root.getLeftChild()); } else { delete(valore, root, root.getRightChild()); } } } protected void delete(T valore, Nodo<T> parent, Nodo<T> nodoCorrente) throws TreeException { if (nodoCorrente == null) { throw new TreeException("Valore inesistente"); } if (valore.compareTo(nodoCorrente.getValue()) < 0) { delete(valore, nodoCorrente, nodoCorrente.getLeftChild()); } else { if (valore.compareTo(nodoCorrente.getValue()) > 0) { delete(valore, nodoCorrente, nodoCorrente.getRightChild()); } else { // nodoCorrente è il nodo che voglio cancellare Nodo<T> temp; if (nodoCorrente.getRightChild() == null) { temp = nodoCorrente.getLeftChild(); } else { temp = nodoCorrente.getRightChild(); insertTree(nodoCorrente.getRightChild(), nodoCorrente.getLeftChild()); } if (parent.getLeftChild() == nodoCorrente) { parent.setLeftChild(temp); } else { parent.setRightChild(temp); } } } } /** * inserisce il sottoalbero addedTreeRoot come elemento più piccolo del * sottoalbero che ha radice in treeRoot se treeRoot è non nullo * * @param treeRoot * @param addedTreeRoot */ private void insertTree(Nodo<T> treeRoot, Nodo<T> addedTreeRoot) { Nodo<T> parent = null; while (treeRoot != null) { parent = treeRoot; treeRoot = treeRoot.getLeftChild(); } if (parent != null) { parent.setLeftChild(addedTreeRoot); } } // e altri metodi da aggiornare… /** * metodo d'istanza per la serializzazione - richiede che T sia anche * Serializable. Solo se richiesto per la serializzazione T implementa anche * Serializable * * @param pathToFile * @throws java.io.IOException */ public void serialize(String pathToFile) throws IOException { try (FileOutputStream fileOut = new FileOutputStream(pathToFile); ObjectOutputStream out = new ObjectOutputStream(fileOut)) { out.writeObject(this); } } /** * * @param pathFromFile * @return * @throws IOException * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") public AlberoBST<T> deserialize(String pathFromFile) throws IOException, ClassNotFoundException { AlberoBST<T> albero; try (FileInputStream fileIn = new FileInputStream(pathFromFile); ObjectInputStream in = new ObjectInputStream(fileIn);) { albero = (AlberoBST<T>) in.readObject(); } return albero; } /** * * @param <T> * @param albero - albero da serializzare * @param f - file su cui effettuare la serializzazione * @throws IOException */ public static <T extends Serializable & Comparable<T>> void serialize(AlberoBST<T> albero, File f) throws IOException { try (FileOutputStream fileOut = new FileOutputStream(f); ObjectOutputStream out = new ObjectOutputStream(fileOut);) { out.writeObject(albero); } } /** * * @param <T> * @param f - file da cui deserializzare l'albero binario di ricerca * @return l'albero binario di ricerca * @throws IOException * @throws ClassNotFoundException */ @SuppressWarnings("unchecked") public static <T extends Serializable & Comparable<T>> AlberoBST<T> deserialize(File f) throws IOException, ClassNotFoundException { AlberoBST<T> albero; try (FileInputStream fileIn = new FileInputStream(f); ObjectInputStream in = new ObjectInputStream(fileIn);) { albero = (AlberoBST<T>) in.readObject(); } return albero; } } //classe VoceDizionario import java.io.Serializable; public class VoceDizionario implements Comparable<VoceDizionario>, Serializable { /** * */ private static final long serialVersionUID = 4165917338641674197L; private String voce; private String significato; public String getVoce() { return voce; } public void setVoce(String voce) { this.voce = voce; } public String getSignificato() { return significato; } public void setSignificato(String significato) { this.significato = significato; } public VoceDizionario(){ voce=""; significato=""; } public VoceDizionario(String voce, String significato){ this.voce=voce; this.significato=significato; } @Override public int compareTo(VoceDizionario o) { return this.getVoce().compareTo(o.getVoce()); } @Override public String toString(){ return "("+this.getVoce()+" = "+ this.getSignificato() +")"; } } Testare la classe AlberoBST con interi, stringhe e con alcune voci di dizionario, come ad esempio: //classe MainTest VoceDizionario v1 = new VoceDizionario("andare", "to go"); VoceDizionario v2 = new VoceDizionario("venire", "to come"); VoceDizionario v3 = new VoceDizionario("corpo del reato", "body of evidence"); VoceDizionario v4 = new VoceDizionario("studiare", "to study"); VoceDizionario v5 = new VoceDizionario("martello", "hammer"); VoceDizionario v6 = new VoceDizionario("spada", "sword"); VoceDizionario v7 = new VoceDizionario("penna", "pen"); VoceDizionario v8 = new VoceDizionario("macchina", "car"); List<VoceDizionario> list= new ArrayList<VoceDizionario>(); list.add(v1); list.add(v2); list.add(v3); list.add(v4); list.add(v5); list.add(v6); list.add(v7); list.add(v8); AlberoBST<VoceDizionario> albero4 = new AlberoBST<VoceDizionario>(list); //effettuare oltre alle solite prove anche un test di serializzazione: //prova serializzazione try { final File f = new File(MainTest.class.getProtectionDomain().getCodeSource().getLocation().getPath( )); String fileName = "albero.ser"; String percorso = f.getCanonicalPath() + File.pathSeparator + fileName; System.out.println("Serializza albero4"); albero4.serialize(percorso); AlberoBST<VoceDizionario> albero5; albero5 = (new AlberoBST<VoceDizionario>()).deserialize(percorso); List<VoceDizionario> lista5; System.out.println("Stampa albero deserializzato in ordine - metodo toArrayList(VisitCriteria.INORDER)"); lista5 = albero5.asArrayListView(VisitCriteria.INORDER); System.out.println(lista5); } catch (IOException | ClassNotFoundException | TreeException e) { e.printStackTrace(); } Diagramma UML del progetto Test3AlberoBST Classe AlberoBST ver.4: progetto Test4AlberoBST Obiettivi: ristrutturare la classe AlberoBST per avere la classe Nodo e l’enum VisitCriteria come inner classes della classe AlberoBST. Nella classe AlberoBST aggiungere i seguenti metodi: /** * Restituisce il nodo contenente il valore * * @param valore * @return il nodo contenente il valore */ protected Nodo getNodo(T valore) { return getNodo(valore, root); } private Nodo getNodo(T valore, Nodo nodoCorrente) { if (nodoCorrente == null) { return null; } if (valore.compareTo(nodoCorrente.getValue()) < 0) { return getNodo(valore, nodoCorrente.getLeftChild()); } else { if (valore.compareTo(nodoCorrente.getValue()) > 0) { return getNodo(valore, nodoCorrente.getRightChild()); } else { return nodoCorrente; } } } Progetto Dizionario: il benchmark definitivo Lo scopo di questo progetto è quello di testare le prestazioni delle diverse strutture dati viste in questo corso quando si ha a che fare con una grande quantità di dati e/o un elevato numero di operazioni di accesso, inserimento o cancellazione. Questo progetto utilizza un dizionario inglese – italiano in formato txt, chiamato inglese_italiano.txt. Il formato del dizionario è molto semplice: la prima riga è la voce in inglese, la riga successiva è il significato in italiano, e poi ancora voce e significato e così via. Il dizionario ha circa 250,000 voci ed è dunque costituito da circa 500,000 righe di testo. Verranno esaminale le prestazioni di ArrayList, Alberi binari di ricerca (BST) e di HashMap con riferimento a operazioni di accesso casuale a voci del dizionario, di inserimento di nuove voci, oppure di ordinamento delle voci del dizionario. Non verrà presa in considerazione la LinkedList dal momento che le sue caratteristiche la rendono utile principalmente in quei contesti nei quali è richiesto un accesso sequenziale alla struttura dati e si vuole avere un’occupazione ottimale di memoria. Le prestazioni di una LinkedList sarebbero sicuramente inferiori a quelle di un ArrayList e delle altre strutture dati prese in considerazione. Per creare le condizioni di test occorre creare un nuovo progetto AlberoBSTDizionario strutturato come segue: Package Dizionario Package tree Con le Classi AlberoBST e TreeException Classe Dizionario import it.onbit.ex.tree.AlberoBST; import it.onbit.ex.tree.TreeException; import import import import import import java.io.BufferedReader; java.io.File; java.io.FileReader; java.io.IOException; java.util.ArrayList; java.util.List; public class Dizionario extends AlberoBST<VoceDizionario> { protected AlberoBST<VoceDizionario> getAlberoDizionario() { return this; } private static final long serialVersionUID = -6020590390391470213L; /** * Crea un albero binario di ricerca con le voci del database * * @param database * - file che contiene le voci del dizionario * @param forceBalancing * - se true l'albero binario di ricerca è bilanciato */ public Dizionario(File database, boolean forceBalancing) { List<VoceDizionario> myList = buildEntryListFromDatabase(database); // creo un albero binario di ricerca bilanciato con le voci del database this.inserisciListaValori(myList, forceBalancing); } /** * Crea un albero binario di ricerca bilanciato con le voci della lista * * @param list */ public Dizionario(List<VoceDizionario> list) { super(list, true); } public boolean inserisciVoce(VoceDizionario vd) { return inserisciValore(vd); } public boolean cercaVoce(VoceDizionario vd) { return ricercaValore(vd); } public boolean cercaVocePerEntry(String entry) { VoceDizionario v = new VoceDizionario(entry, ""); return ricercaValore(v); } public VoceDizionario getVoce(String entry) { VoceDizionario v = new VoceDizionario(entry, ""); AlberoBST<VoceDizionario>.Nodo tmp = getNodo(v); if (tmp!=null) return tmp.getValue(); else return null; } public String getSignificato(String entry) { VoceDizionario v = new VoceDizionario(entry, ""); AlberoBST<VoceDizionario>.Nodo tmp = getNodo(v); if (tmp!=null) return tmp.getValue().getSignificato(); else return ""; } public boolean eliminaVoce(VoceDizionario vd) { boolean flag = false; if (ricercaValore(vd)) { try { cancellaValore(vd); flag = true; } catch (TreeException e) { e.printStackTrace(); } } return flag; } public boolean eliminaVocePerEntry(String entry) { boolean flag = false; VoceDizionario v = new VoceDizionario(entry, ""); if (ricercaValore(v)) { try { cancellaValore(v); flag = true; } catch (TreeException e) { e.printStackTrace(); } } return flag; } /** * Il database è un file in formato testo con la seguente struttura: una * linea è la voce in inglese, la linea successiva è la traduzione in * italiano * * @param dataBase * @return */ protected List<VoceDizionario> buildEntryListFromDatabase(File dataBase) { //BufferedReader fileReader = null; List<VoceDizionario> listaDizionario = new ArrayList<>(); String voce; String significato; try (BufferedReader fileReader = new BufferedReader(new FileReader(dataBase))){ // Read the file line by line while ((voce = fileReader.readLine()) != null && (significato = fileReader.readLine()) != null) { listaDizionario.add(new VoceDizionario(voce, significato)); } } catch (IOException e) { e.printStackTrace(); } return listaDizionario; } } ArrayList vs. AlberoBST vs. HashMap Scrivere la classe MainTest con i seguenti metodi (da implementare): /** * Metodo statico ricorsivo che restituisce la VoceDizioanrio che contiene l'entry specificata * @param orderedList la lista ordinata di VoceDizionario * @param entry l'entry di cui si vuole la VoceDizionario corrispondente * @param left indice sinistro della ricerca dicotomica * @param right indice destro della ricerca dicotomica * @return la VoceDizionario che contiene l'entry specificata, null se l'entry non è presente nella lista */ public static VoceDizionario ricercaDicotomica(List<VoceDizionario> orderedList, String entry, int left, int right); /** * Metodo statico che inserisce una VoceDizionario nella lista ordinata, mantenendo l'ordinamento della lista * @param orderedList la lista di VoceDIzionario * @param vd la voce da inserire * @param left indice sinistro * @param right indice destro */ public static void inserisciInOrdineInListaOrdinata(List<VoceDizionario> orderedList, VoceDizionario vd, int left, int right); public static void main(String[] args); //il main apre il file inglese_italiano.txt //effettua i seguenti test: TEST 1: confronto tra i tempi di accesso di una lista non ordinata e un albero bilanciato; la lista è scandita sequenzialmente TEST 2: confronto tra i tempi di accesso di una lista ordinata e un albero bilanciato; la lista ordinata è scandita con criterio di ricerca dicotomica TEST 3: confronto tra i tempi di inserimento in una lista ordinata e un albero (non) bilanciato; La lista ordinata è mantenuta ordinata ad ogni inserimento. L'inserimento è fatto in due passi: Passo 1: applicando la ricerca dicotomica si determina la posizione dove si dovrebbe inserire la nuova voce Passo 2: la nuova voce è inserita nella lista nella posizione stabilita al passo 1 Le voci inserite sono generate in modo random TEST 4: confronto tra i tempi di accesso in una hashmap e in un albero bilanciato TEST 5: confronto tra i tempi di inserimento in una hashmap e in un albero (non) bilanciato TEST 6: confronto tra i tempi di ordinamento degli elementi di una hashmap e di un albero (non) bilanciato Suggerimento: usare la funzione nanoTime per calcolare quanto tempo impiega un metodo, come nel seguente esempio: import java.util.Random; import java.util.concurrent.TimeUnit; //nel main… String fileName = "inglese_italiano.txt"; String percorso = MainTest.class.getProtectionDomain().getCodeSource().getLocation().getPath() + File.separatorChar + fileName; File fileDatabase = new File(percorso); // test 1 - tempo di accesso di un elemento casuale // confrontiamo le prestazioni di un albero bilanciato e di una // lista ordinata System.out.println("***********************************************"); System.out .println("TEST 1: confronto tra i tempi di accesso di una lista non ordinata e un albero bilanciato"); System.out.println("la lista è scandita sequenzialmente"); Dizionario diz = new Dizionario(fileDatabase, true); // Ottengo l'albero del dizionario AlberoBST<VoceDizionario> al = diz.getAlberoDizionario(); System.out.println("livello massimo albero " + al.getLivelloMassimo()); System.out.println("livello minimo albero " + al.getLivelloMinimo()); System.out.println("numero nodi albero " + al.getNumeroNodi()); System.out.println("l'albero è " + (al.isBalanced() ? "bilanciato" : "non bilanciato") + "\n"); // Creo una lista con le voci del dizionario List<VoceDizionario> l = al.asArrayListView(AlberoBST.VisitCriteria.INORDER); // mischio gli elementi nella lista Collections.shuffle(l); Random rand = new Random(); int testSize = 200000; long estimatedTimeList = 0; long estimatedTimeTree = 0; for (int k = 0; k < testSize; k++) { // scelgo un indice a caso int randomNum = rand.nextInt(l.size()); // ottengo una voce del dizionario a caso VoceDizionario vRandom = l.get(randomNum); // parte il test sull'albero bilanciato long startTimeTree = System.nanoTime(); diz.cercaVoce(vRandom); //dizBalanced.getVoce(vRandom.getVoce()); long endTimeTree = System.nanoTime(); estimatedTimeTree += endTimeTree - startTimeTree; // parte il test sulla lista non ordinata long startTimeList = System.nanoTime(); l.contains(vRandom); //l.get(l.indexOf(vRandom)); long endTimeList = System.nanoTime(); estimatedTimeList += endTimeList - startTimeList; }// end of for System.out.println("Tempo di accesso complessivo sulla lista (per " + testSize + " accessi): " + TimeUnit.MICROSECONDS.convert(estimatedTimeList, TimeUnit.NANOSECONDS) + " us"); System.out.println("Tempo di accesso complessivo sull'albero (per " + testSize + " accessi): " + TimeUnit.MICROSECONDS.convert(estimatedTimeTree, TimeUnit.NANOSECONDS) + " us"); System.out.format("Tempo di accesso albero/ tempo accesso lista: %.3f%% ", (1.0 * estimatedTimeTree / estimatedTimeList) * 100); System.out.println("\nVince "+(estimatedTimeTree<estimatedTimeList?"l'albero":"la lista")); //to be continued… I test andrebbero fatti un elevato numero di volte: Test1, Test2 e Test4: nell’ordine 100,000 volte Test3 e Test5: nell’ordine di 10,000 volte Test6: 1 volta Di seguito sono riportati alcuni risultati, da usare come riferimento per i propri benchmark. Si osservi che i tempi assoluti dipendono dal processore e in genere dall’HW utilizzato. I valori percentuali non lo sono. Si osservi che i valori che si ottengono dai test possono essere variabili da test a test, ma l’andamento statistico non varia. *********************************************** TEST 1: confronto tra i tempi di accesso di una lista non ordinata e un albero bilanciato la lista è scandita sequenzialmente livello massimo albero 17 livello minimo albero 16 numero nodi albero 249932 l'albero è bilanciato Tempo di accesso complessivo sulla lista (per 200000 accessi): 9105347 us Tempo di accesso complessivo sull'albero (per 200000 accessi): 267359 us Tempo di accesso albero/ tempo accesso lista: 2,936% Vince l'albero *********************************************** TEST 2: confronto tra i tempi di accesso di una lista ordinata e un albero bilanciato la lista ordinata è scandita con criterio di ricerca dicotomica Tempo di accesso complessivo sulla lista (per 200000 accessi): 126169 us Tempo di accesso complessivo sull'albero (per 200000 accessi): 246853 us Tempo di accesso lista/ tempo accesso albero: 51,111% Vince la lista *********************************************** TEST 3: confronto tra i tempi di inserimento in una lista ordinata e un albero (non) bilanciato La lista ordinata è mantenuta ordinata ad ogni inserimento. L'inserimento è fatto in due passi: passo 1: applicando la ricerca dicotomica si determina la posizione dove si dovrebbe inserire la nuova voce passo 2: la nuova voce è inserita nella lista nella posizione stabilita al passo 1 Le voci inserite sono generate in modo random numero elementi lista: 249932 Tempo di inserimento complessivo sulla lista ordinata (per 10000 inserimenti): 296268 us Tempo di inserimento complessivo sull'albero (per 10000 inserimenti): 16454 us Tempo di inserimento albero/ tempo inserimento lista ordinata: 5,554% Vince l'albero Dopo 10000 inserimenti l'albero del dizionario si trova nelle seguenti condizioni: livello massimo albero 21 livello minimo albero 16 numero nodi albero 259932 l'albero è non bilanciato Dopo 10000 inserimenti la lista ordinata si trova nelle seguenti condizioni: numero elementi lista: 259932 *********************************************** TEST 4: confronto tra i tempi di accesso in una hashmap e in un albero bilanciato livello massimo albero 17 livello minimo albero 16 numero nodi albero 249932 l'albero è bilanciato numero elementi nella mappa: 249932 Tempo di accesso complessivo sulla mappa (per 200000 accessi): 21708 us Tempo di accesso complessivo sull'albero (per 200000 accessi): 254161 us Tempo di accesso mappa/ tempo accesso albero: 8,541% Vince la mappa *********************************************** TEST 5: confronto tra i tempi di inserimento in una hashmap e in un albero (non) bilanciato numero elementi nella mappa: 249932 Tempo di inserimento complessivo sulla mappa (per 10000 inserimenti): 3531 us Tempo di inserimento complessivo sull'albero (per 10000 inserimenti): 17986 us Tempo di inserimento mappa/ tempo inserimento albero: 19,634% Vince la mappa Dopo 10000 inserimenti l'albero del dizionario si trova nelle seguenti condizioni: livello massimo albero 21 livello minimo albero 16 numero nodi albero 259932 l'albero è non bilanciato Dopo 10000 inserimenti la hash map si trova nelle seguenti condizioni: numero elementi hash map: 259932 *********************************************** TEST 6: confronto tra i tempi di ordinamento degli elementi di una hashmap e di un albero (non) bilanciato numero elementi nella mappa: 259932 numero elementi nell'albero: 259932 Tempo per ottenere una lista ordinata da una mappa: 335475 us Tempo per ottenere una lista ordinata da un albero: 12326 us Tempo di ordinamento albero/ tempo ordinamento mappa: 3,674% Vince l'albero Tipologie di Alberi binari di ricerca https://en.wikipedia.org/wiki/Binary_search_tree#Types There are many types of binary search trees. AVL trees and red-black trees are both forms of selfbalancing binary search trees. A splay tree is a binary search tree that automatically moves frequently accessed elements nearer to the root. In a treap (tree heap), each node also holds a (randomly chosen) priority and the parent node has higher priority than its children. Tango trees are trees optimized for fast searches. Alberi nelle API di Java Nella API di Java ci sono due strutture dati che implementano il concetto di albero: TreeMap e TreeSet. You choose a map in the situation where you have keys and you want to associate each key with a value and later query (or change) the associated value of each key. You choose a set when you want to have a set of values and you want to add and remove items from that set and check whether a given item is included in that set. L’albero binario AlberoBST che abbiamo implementato è una sorta di TreeSet. TreeMap<K,V> public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, Serializable A Red-Black tree based NavigableMap implementation. The map is sorted according to the natural ordering of its keys, or by a Comparator provided at map creation time, depending on which constructor is used. This implementation provides guaranteed log(n) time cost for the containsKey, get, put and remove operations. Algorithms are adaptations of those in Cormen, Leiserson, and Rivest's Introduction to Algorithms. Note that the ordering maintained by a tree map, like any sorted map, and whether or not an explicit comparator is provided, must be consistent with equals if this sorted map is to correctly implement the Map interface. (See Comparable or Comparator for a precise definition of consistent with equals.) This is so because the Map interface is defined in terms of the equals operation, but a sorted map performs all key comparisons using its compareTo (or compare) method, so two keys that are deemed equal by this method are, from the standpoint of the sorted map, equal. The behavior of a sorted map is well-defined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Map interface. TreeSet<E> public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, Serializable A NavigableSet implementation based on a TreeMap. The elements are ordered using their natural ordering, or by a Comparator provided at set creation time, depending on which constructor is used. This implementation provides guaranteed log(n) time cost for the basic operations ( add, remove and contains). Note that the ordering maintained by a set (whether or not an explicit comparator is provided) must be consistent with equals if it is to correctly implement the Set interface. (See Comparable orComparator for a precise definition of consistent with equals.) This is so because the Set interface is defined in terms of the equals operation, but a TreeSet instance performs all element comparisons using its compareTo (or compare) method, so two elements that are deemed equal by this method are, from the standpoint of the set, equal. The behavior of a set is welldefined even if its ordering is inconsistent with equals; it just fails to obey the general contract of the Set interface.