Programmazione Parametrica ( a.k.a. Generics )
Introduzione ai meccanismi e concetti della programmazione parametrica Generics e relationi di sottotipo wildcards generics e vincoli Implementazione di classi e metodi parametrici Supporto per i generics nella JVM
Programmazione polimorfa Polimorfo ~ multiforme, di molti tipi Programmazione polimorfa: creazione di costrutti (classi e metodi) che possono essere utilizzati in modo uniforme su dati di tipo diverso In Java, tradizionalmente ottenuta mediante i meccanismi di sottotipo ed ereditarietà Da Java 1.5. anche mediante i meccanismi di parametrizzazione di tipo (a.k.a. generics)
Variabili di Tipo Le variabili (o parametri) di tipo pemettono di creare astrazioni di tipo Classico caso di utilzzo nelle classi “Container” E = variabile di tipo astrae (e rappresenta) il tipo delle componenti public class ArrayList<E> { public ArrayList() { . . . } public void add(E element) { . . . } . . . } Continua
Variabili di Tipo Possono essere istanziate con tipi classe o interfaccia Vincolo: tipi che istanziano variabili di tipo non possono essere primitivi (devono essere tipi riferimento) Classi wrapper utili allo scopo ArrayList<BankAccount> ArrayList<Measurable> ArrayList<double> // No! ArrayList<Double>
Variabili di tipo e controlli di tipo Utilizzare variabili di tipo nella programmazione permette maggiori controlli sulla correttezza dei tipi in fase di compilazione Aumenta quindi la solidità e robustezza del codice Continua
Variabili di tipo e controlli di tipo Un classico caso di utilizzo di containers Il cast è problematico, per vari motivi verboso, fonte di errori a run time Ma necessario per la compilazione e per localizzare l’eventuale errore a run time List intList = new LinkedList(); intList.add(new Integer(0)); Integer x = (Integer) intList.get(0); Continua
Variabili di tipo e controlli di tipo Container generici: più sintetici ed eleganti Compilatore può stabilire un invariante sugli elementi della lista garantire l’assenza di errori a run-time in forza di quell’invariante. List<Integer> intList = new LinkedList<Integer>(); intList.add(new Integer(0)); Integer x = intList.get(0); Continua
Variabili di tipo e controlli di tipo Ora non è possibile aggiungere una stringa ad intlist:List<Integer> Le variabili di tipo rendono il codice parametrico più robusto e semplice da leggere e manutenere List<Integer> intList = new LinkedList<Integer>(); intList.add(new Integer(0)); Integer x = intList.get(0);
Classi parametriche: definizione Un frammento delle interfacce List e Iterator nel package java.util.* // nulla di particolare, a parte i parametri // tra parentesi angolate public interface List<E> { void add(E x); Iterator<E> iterator(); } public interface Iterator<F> F next(); boolean hasNext(); Nulla di speciale, a parte la presenza dei parametri di tipo
Classi parametriche: uso Quando utilizziamo un tipo parametrico, tutte le occorrenze dei parametri formali sono rimpiazzate dall’argomento (parametro attuale) Meccanismo simile a quello del passaggio dei parametri in un metodo Diversi usi generano tipi diversi Ma . . . classi parametriche compilate una sola volta danno luogo ad un unico file .class Nulla di speciale, a parte la presenza dei parametri di tipo
Sintassi: uso GenericClassName<Type1, Type2, . . .> Esempio: ArrayList<BankAccount> HashMap<String, Integer> Scopo: Fornire tipo specifici per ciascuna delle variabili di tipo introdotte nella dichiarazione
Esempio: Pair<T,S> Una semplice classe parametrica per rappresentare coppie di oggetti public class Pair<T, S> { public Pair(T firstElement, S secondElement) { first = firstElement; second = secondElement; } public T getFirst() { return first; } public S getSecond() { return second; } private T first; private S second; } Continua
Esempio: Pair<T,S> Una semplice classe parametrica per rappresentare coppie di oggetti: I metodi getFirst e getSecond restituiscono il primo e secondo elemento, con i tipi corrispondenti Pair<String, BankAccount> result = new Pair<String, BankAccount> ("Harry Hacker", harrysChecking); String name = result.getFirst(); BankAccount account = result.getSecond();
Variabili di tipo: convenzioni Variabile Significato Inteso E Tipo degli elementi in una collezione K Tipo delle chiavi in una mappa V Tipo dei valori in una mappa T,S,U Tipi generici
Esempio: LinkedList<E> public class LinkedList<E> { . . . public E removeFirst() { if (first == null) throw new NoSuchElementException(); E element = first.data; first = first.next; return element; } . . . private Node first; private class Node { E data; Node next; } } Continua
Esempio: LinkedList<E> Notiamo la struttura della classe ausiliaria che specifica la struttura dei nodi Se la classe è interna, come in questo caso, non serve alcun accorgimento all’interno di Node possiamo utilizzare il tipo E, il cui scope è tutta la classe Se invece la classe è esterna, dobbiamo renderla generica
Esempio: LinkedList<E> class Node<F> { F data; Node next; } public class LinkedList<E> { . . . public E removeFirst() { if (first == null) throw new NoSuchElementException(); E element = first.data; first = first.next; return element; } . . . private Node<E> first; } Continua
Generics e sottotipi I meccanismi di subtyping si estendono alle classi generiche C<T> <: I<T> per qualunque T Analogamente: C<T> <: I per qualunque T Sembra tutto facile, MA . . . class C<T> implements I<T> { . . . } class C<T> implements I { . . . }
Generics e sottotipi Consideriamo La prima istruzione è legale, la seconda è più delicata … Number è una classe che ha Integer , Double e altre classi wrapper come sottotipi Per capire se la seconda istruzione sia da accettare come legale continuiamo con l’esempio … List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; Continua
Generics e sottotipi Come si vede abbiamo un problema List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; // type error ln.add(3.14); Integer i = li.get(0); // uh oh ... Come si vede abbiamo un problema nella terza istruzione inseriamo un Double nella quarta estraiamo un Integer ! Il vero problema è nella seconda istruzione soluzione: errore di compilazione per l’assegnamento Continua
Generics e sottotipi In generale, dati due tipi A e B , ed tipo generico C<T> abbiamo che: Quindi, per le stesse ragioni di prima Come abbiamo visto questo è necessario per garantire la correttezza A ≤ B NON implica C<A> ≤ C<B> Set<Integer> NON è sottotipo di Set<Object> Continua
A ≤ B NON implica C<A> ≤ C<B> Generics e sottotipi In generale, dati due tipi A e B , ed tipo generico C<T> abbiamo che: MA … A ≤ B NON implica C<A> ≤ C<B> A ≤ B implica A[] ≤ B[] Continua
Generics e sottotipi Integer[] ai = new Integer[10] Number[] an = ai; // type OK an[0] = 3.14; // ArrayStoreException Integer i = ai[0]; // uh oh ... Continua
Generics e sottotipi Le limitazione sulle relazioni di sottotipo sono contro-intuitive uno degli aspetti più complessi dei generics Non solo … sono spesso anche troppo restrittive illustriamo con un esempio Continua
Generics e sottotipi Stampa degli elementi di una collezione Primo tentativo Inutile per stampare gli elementi di una generica Collection<T> Collection<Object> non è il supertipo di tutte le collezioni static void printCollection(Collection<Object> c) { for (Object e:c) System.out.println(e); } Continua
Wildcards Stampa degli elementi di una collezione Secondo tentativo Collection<?> è il supertipo di tutte le Collections la wildcard ? indica un qualche tipo, non specificato static void printCollection(Collection<?> c) { for (Object e:c) System.out.println(e); } Continua
Wildcards Possiamo estrarre gli elementi di c al tipo Object Corretto perché, qualunque sia il loro vero tipo, sicuramente è sottotipo di Object void printCollection(Collection<?> c) { for (Object e:c) System.out.println(e); } Continua
Wildcards D’altra parte … Poichè non sappiamo esattamente quale tipo indica ?, non possiamo inserire elementi nella collezione In generale, non possiamo modificare valori che hanno tipo ? Collection<?> c = new ArrayList<String>(); c.add(new String()); // errore di compilazione! Continua
Domanda Date un esempio di codice che causerebbe errore in esecuzione se permettessimo di aggiungere elementi a Collection<?>
Risposta Collection<Integer> ci = new ArrayList<Integer>; Colletion<?> c = ci; c.add(“a string”); // non compila ci.get(0).intValue(); L’ultima istruzione invocherebbe intValue() sul primo elemento di ci ma quell’elemento ha tipo String … Il compilatore previene l’errore, rigettando la add()
Wilcards con vincoli (bounded) Shapes: (again!) interface Shape { public void draw(Graphics g); } class Circle extends Shape private int x, y, radius; public void draw(Graphics g) { ... } class Rectangle extends Shape private int x, y, width, height; Continua
Wilcards con vincoli (bounded) Graphics e il metodo draw() Solito problema: drawAll() non può essere invocato su una List<Circle> public class Graphics { // disegna una shape public void draw(Shape s) { s.draw(this); } // disegna tutte le shapes di una lista public void drawAll(List<Shape> shapes) { for (Shape s:shapes) s.draw(this) } . . . Continua
Bounded Wilcards Quello che ci serve è un metodo che accetti liste di qualunque (sotto) tipo di Shape List<? extends Shape> bounded wildcard indica un tipo sconosciuto, sottotipo di Shape il bound può essere qualunque tipo riferimento (classe o interfaccia) Ora il metodo ha la flessibilità necessaria e desiderata void drawAll(List<? extends Shape> shapes) { ... } Continua
Bounded Wilcards Graphics e il metodo draw() public class Graphics { // disegna una shape public void draw(Shape s) { s.draw(this); } // disegna tutte le shapes di una lista public void drawAll(List<? extends Shape> shapes) for (Shape s:shapes) s.draw(this) } . . . Continua
Bounded Wilcards Attenzione: c’è sempre un prezzo da pagare Non possiamo modificare strutture con questi tipi [ perché? ] void addRectangle(List<? extends Shape> shapes) { // errore di compilazione shapes.add(new Rectangle()); }
Metodi Generici Metodi che dipendono da una variabile di tipo Possono essere definiti all’interno di qualunque classe, generica o meno N.B. Evitiamo List<Object> perché renderebbe il metodo non utilizzabie su liste arbitrarie /* trasforma un array in una lista, copiando * tutti gli elementi di a in l */ static void array2List(Object[] a, List<?> l){ . . . } Continua
Metodi Generici Al solito però . . . . . . non possiamo aggiungere elementi ad una struttura (o modificare) con elementi di tipo wildcard /* trasforma un array in una lista, copiando * tutti gli elementi di a in l */ static void array2List(Object[] a, List<?> l) { for (Object o : a) l.add(o) // compiler error } Continua
Metodi Generici Soluzione: rendiamo il metodo parametrico possiamo invocare questo metodo con una qualunque lista il cui tipo sia supertipo del tipo base dell’array purché sia un tipo riferimento /* trasforma un array in una lista, copiando * tutti gli elementi di a in l */ static <T> void array2List(T[] a, List<T> l) { for (T o : a) l.add(o) }
Invocazione di metodi generici Nell’invocazione di un metodo generico non è necessario passare l’argomento di tipo il compilatore inferisce il tipo, se esiste, dai tipi degli argomenti del metodo
Invocazione di metodi generici Object[] oa = new Object[100]; Collection<Object> co = new ArrayList<Object>(); fromArrayToCollection(oa, co); // T = Object (inferito) String[] sa = new String[100]; Collection<String> cs = new ArrayList<String>(); fromArrayToCollection(sa, cs); // T = String (inferito) fromArrayToCollection(sa, co); // T = Object (inferito) Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection<Number> cn = new ArrayList<Number>(); fromArrayToCollection(ia, cn); // T = Number (inferito) fromArrayToCollection(fa, cn); // T = Number (inferito) fromArrayToCollection(na, cn); // T = Number (inferito) fromArrayToCollection(na, co); // T = Object (inferito) fromArrayToCollection(na, cs); // compiler error Continua
Wildarcds vs variabili di tipo Ci sono situazioni in cui è possibili usare equivalentemente wildcards e variabili di tipo. Nella libreria Collection troviamo interface Collection<E> { public boolean containsAll(Collection<?> c); public boolean addAll(Collection<? extends E> c); public boolean addAll(Collection<E> c); . . . } Continua
Wildarcds vs variabili di tipo Queste specifiche possono essere espresse equivalentemente con metodi parametrici Il secondo metodo è parametrico in qualunque sottotipo di E i bounds si possono utilizzare anche con variabili, non solo con wildcards interface Collection<E> { public <T> boolean containsAll(Collection<T> c); public <T extends E> boolean addAll(Collection<T> c); . . . } Continua
Wildarcds vs variabili di tipo Wildcards e variabili di tipo possono coesistere Notiamo la dipendenza tra i tipi dei due parametri: il tipo della sorgente deve essere un sottotipo del tipo della destinazione interface Collection<E> { public static <T> void copy(List<T> dest, List<? extends T> src) . . . } Continua
Wildarcds vs variabili di tipo Potremmo analogamente riformulare in modo da evitare le wildcards Come scegliere tra le due soluzioni? interface Collection<E> { public static <T, S extends T> void copy(<List<T> dest, List<S> src) . . . } Continua
Wildarcds vs variabili di tipo In generale, preferiamo le wildcards quando entrambe le soluzioni sono possibili Possiamo darci la seguente “rule of thumb” se una variabile di tipo ha una unica occorrenza nella specifica di un metodo e il tipo non è il target di un operazione di modifica utilizziamo una wildcard al posto della variabile
Generics e “erasure” I tipi generici sono significativi a compile-time La JVM opera invece con tipi “raw” Il tipo raw è ottenuto da un tipo generico mediante un processo detto erasure che rimuove le variabili di tipo il bycode generato da un tipo generico è lo stesso che viene generato dal corrispondente tipo raw.
Generics e “erasure” Generano lo stesso bytecode List<String> words = new ArrayList<String>(); words.add(“hi”); words.add(“there”); String welcome = words.get(0) + words.get(1); List words = new ArrayList(); words.add(“hi”); words.add(“there”); String welcome = (String)words.get(0) + (String)words.get(1);
Generics e “erasure” Cast-iron guarantee i cast impliciti che vengono aggiunti dalla compilazione di codice generico non falliscono mai.
Generics e Array Non è possibile creare array generici Ecco perché: class MyClass<T> { T[] contents = new T[100]; // Non compila public void showTheProblem() { Object[] objs = contents; objs[0] = new String(); // no ArrayStoreException T bump = contents[0]; // ClassSclassException }
Variabili di Tipo e Bounds Abbiamo visto che possiamo definire bounds anche per variabili di tipo (non solo wildcards) Un caso paradigmatico public static <T extends Comparable<T>> T max(Collection<T> coll) { T candidate = coll.iterator().next(); for (T e : coll) if candidate.compareTo(e) < 0) candidate = e; return candidate; }
Variabili di Tipo e Bounds Il bound su una variabile impone vincoli sulla variabile, determinando quali metodi possono essere utilizzati su valori del tipo variabile Qui il bound è ricorsivo: informa che i valori con cui operiamo forniscono un metodo compareTo() che gli argomenti del metodo devono essere dello stesso tipo dei valori public static <T extends Comparable<T>> T max(T max(List <T> coll)
File LinkedList.java 019: /** 019: /** 020: Returns the first element in the linked list. 021: @return the first element in the linked list 022: */ 023: public E getFirst() 024: { 025: if (first == null) 026: throw new NoSuchElementException(); 027: return first.data; 028: } 029: 030: /** 031: Removes the first element in the linked list. 032: @return the removed element 033: */ 034: public E removeFirst() 035: { Continua
File LinkedList.java 036: if (first == null) 037: throw new NoSuchElementException(); 038: E element = first.data; 039: first = first.next; 040: return element; 041: } 042: 043: /** 044: Adds an element to the front of the linked list. 045: @param element the element to add 046: */ 047: public void addFirst(E element) 048: { 049: Node newNode = new Node(element,first); 052: first = newNode; 053: } Continua
File LinkedList.java 054: 055: /** 055: /** 056: Returns an iterator for iterating through this list. 057: 058: */ 059: public ListIterator<E> listIterator() 060: { 061: return new LinkedListIterator(); 062: } 063: 064: private Node first; 065: 066: private class Node 067: { 068: E data; 069: Node next; 070: } 071: Continua
LinkedListIterator La definiamo come classe interna di LinkedList<E> Implemental’interfaccia ListIterator<E> Ha accesso al campo first e alla classe interna Node
Classe LinkedListIterator 072: private class LinkedListIterator implements ListIterator<E> 073: { 074: /** 075: Costruisce un iteratore posizionato sul primo 076: elemento della lista 077: */ 078: public LinkedListIterator() 079: { 080: last = null; // ultimo nodo visitato 081: previous = null; // precedente a position 082: } 083: Continua
LinkedListIterator – next() 084: /** 085: Posizione l’iteratore sul prossimo. 086: @return il prossimo elemento 087: */ 088: public E next() 089: { 090: if (!hasNext()) 091: throw new NoSuchElementException(); 092: previous = last; // Remember for remove 093: 094: if (last == null) 095: last = first; 096: else 097: last = last.next; 098: 099: return last.data; 100: } 101: 102: Continua
LinkedListIterator – hasNext() 102: /** 103: Testa se c’è un elemento dopo l’ultimo 104: visitato. 105: @return true se esiste un elemento dopo 106: l’ultimo visitato 107: */ 108: public boolean hasNext() 109: { 110: if (last == null) 111: return first != null; 112: else 113: return last.next != null; 114: } 115: 116: Continua
LinkedListIterator – add() 117: /** Aggiunge un elemento dopo last e sposta 118: l’iteratore sul prossimo elemento. 119: */ 121: public void add(E element) 122: { 123: if (last == null) 124: { 125: addFirst(element); last = first; 127: } 128: else 129: { 130: Node newNode = new Node(); 131: newNode.data = element; 132: newNode.next = last.next;// (1) 133: last.next = newNode; // (2) 134: last = newNode; // (3) 135: } 136: previous = last; // (4) 137: } Continua
LinkedListIterator – add() last
LinkedListIterator – remove() 140: /** Rimuove l’elemento puntato da last. Può essere 141: invocato solo dopo una chiamata a next() 142: */ 143: public void remove() 144: { 145: if (previous == last) 146: throw new IllegalStateException(); 147: 148: if (last == first) 149: { 150: removeFirst(); 151: } 152: else 153: { 154: previous.next = last.next; // (1) 155: } 156: last = previous; // (2) 157: } Continua
LinkedListIterator –remove() last
Classe LinkedListIterator 159: /** 160: Sets the last traversed element to a different 161: value. 162: @param element the element to set 163: */ 164: public void set(E element) 165: { 166: if (position == null) 167: throw new NoSuchElementException(); 168: position.data = element; 169: } 170: 171: private Node position; 172: private Node previous; 173: } // end LinkedListIterator 174: } // end LinkedList
File ListIterator.java 01: /** 02: A list iterator allows access of a position in a linked 03: list. This interface contains a subset of the methods 04: of the standard java.util.ListIterator interface. The 05: methods for backward traversal are not included. 06: */ 07: public interface ListIterator<E> 08: { 09: /** 10: Moves the iterator past the next element. 11: @return the traversed element 12: */ 13: E next(); 14: 15: /** 16: Tests if there is an element after the iterator 17: position. Continua
File ListIterator.java 18: @return true if there is an element after the iterator 19: position 20: */ 21: boolean hasNext(); 22: 23: /** 24: Adds an element before the iterator position 25: and moves the iterator past the inserted element. 26: @param element the element to add 27: */ 28: void add(E element); 29: 30: /** 31: Removes the last traversed element. This method may 32: only be called after a call to the next() method. 33: */ Continua
File ListIterator.java 34: void remove(); 35: 36: /** 37: Sets the last traversed element to a different 38: value. 39: @param element the element to set 40: */ 41: void set(E element); 42: }