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” public class ArrayList<E> { public ArrayList() { . . . } public void add(E element) { . . . } . . . } • E = variabile di tipo astrae (e rappresenta) il tipo delle componenti Continua Variabili di Tipo • Possono essere istanziate con tipi classe o interfaccia ArrayList<BankAccount> ArrayList<Measurable> • Vincolo: tipi che istanziano variabili di tipo non possono essere primitivi (devono essere tipi riferimento) ArrayList<double> // No! • Classi wrapper utili allo scopo 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 List intList = new LinkedList(); intList.add(new Integer(0)); Integer x = (Integer) intList.get(0); • 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 Continua Variabili di tipo e controlli di tipo • Container generici: più sintetici ed eleganti List<Integer> intList = new LinkedList<Integer>(); intList.add(new Integer(0)); Integer x = intList.get(0); • Compilatore può stabilire un invariante sugli elementi della lista garantire l’assenza di errori a run-time in forza di quell’invariante. Continua Variabili di tipo e controlli di tipo List<Integer> intList = new LinkedList<Integer>(); intList.add(new Integer(0)); Integer x = intList.get(0); • 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 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(); } 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 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: Pair<String, BankAccount> result = new Pair<String, BankAccount> ("Harry Hacker", harrysChecking); • I metodi getFirst e getSecond restituiscono il primo e secondo elemento, con i tipi corrispondenti 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 class C<T> implements I<T> { . . . } • C<T> <: I<T> per qualunque T • Analogamente: class C<T> implements I { . . . } • C<T> <: I per qualunque T • Sembra tutto facile, MA . . . Generics e sottotipi • Consideriamo List<Integer> li = new ArrayList<Integer>(); List<Number> ln = li; • 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 … Continua Generics e sottotipi 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: A ≤ B NON implica C<A> ≤ C<B> • Quindi, per le stesse ragioni di prima Set<Integer> NON è sottotipo di Set<Object> • Come abbiamo visto questo è necessario per garantire la correttezza Continua Generics e sottotipi • In generale, dati due tipi A e B , ed tipo generico C<T> abbiamo che: A ≤ B NON implica C<A> ≤ C<B> • MA … 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 static void printCollection(Collection<Object> c) { for (Object e:c) System.out.println(e); } Inutile per stampare gli elementi di una generica Collection<T> Collection<Object> non è il supertipo di tutte le collezioni Continua Wildcards • Stampa degli elementi di una collezione • Secondo tentativo static void printCollection(Collection<?> c) { for (Object e:c) System.out.println(e); } Collection<?> è il supertipo di tutte le Collections la wildcard ? indica un qualche tipo, non specificato Continua Wildcards void printCollection(Collection<?> c) { for (Object e:c) System.out.println(e); } • Possiamo estrarre gli elementi di c al tipo Object • Corretto perché, qualunque sia il loro vero tipo, sicuramente è sottotipo di Object Continua Wildcards • D’altra parte … Collection<?> c = new ArrayList<String>(); c.add(new String()); // errore di compilazione! • Poichè non sappiamo esattamente quale tipo indica ?, non possiamo inserire elementi nella collezione • In generale, non possiamo modificare valori che hanno tipo ? 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; public void draw(Graphics g) { ... } } Continua Wilcards con vincoli (bounded) • 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<Shape> shapes) { for (Shape s:shapes) s.draw(this) } . . . } • Solito problema: drawAll() non può essere invocato su una List<Circle> Continua Bounded Wilcards • Quello che ci serve è un metodo che accetti liste di qualunque (sotto) tipo di Shape void drawAll(List<? extends Shape> shapes) { ... } • 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 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 void addRectangle(List<? extends Shape> shapes) { // errore di compilazione shapes.add(new Rectangle()); } • Non possiamo modificare strutture con questi tipi [ perché? ] Metodi Generici • Metodi che dipendono da una variabile di tipo • Possono essere definiti all’interno di qualunque classe, generica o meno /* trasforma un array in una lista, copiando * tutti gli elementi di a in l */ static void array2List(Object[] a, List<?> l){ . . . } • N.B. Evitiamo List<Object> perché renderebbe il metodo non utilizzabie su liste arbitrarie Continua Metodi Generici • Al solito però . . . /* 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 } • . . . non possiamo aggiungere elementi ad una struttura (o modificare) con elementi di tipo wildcard Continua Metodi Generici • Soluzione: rendiamo il metodo parametrico /* 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) } • possiamo invocare questo metodo con una qualunque lista il cui tipo sia supertipo del tipo base dell’array purché sia un tipo riferimento 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 interface Collection<E> { public <T> boolean containsAll(Collection<T> c); public <T extends E> boolean addAll(Collection<T> c); . . . } • Il secondo metodo è parametrico in qualunque sottotipo di E i bounds si possono utilizzare anche con variabili, non solo con wildcards Continua Wildarcds vs variabili di tipo • Wildcards e variabili di tipo possono coesistere interface Collection<E> { public static <T> void copy(List<T> dest, List<? extends T> src) . . . } • Notiamo la dipendenza tra i tipi dei due parametri: il tipo della sorgente deve essere un sottotipo del tipo della destinazione Continua Wildarcds vs variabili di tipo • Potremmo analogamente riformulare in modo da evitare le wildcards interface Collection<E> { public static <T, S extends T> void copy(<List<T> dest, List<S> src) . . . } • Come scegliere tra le due soluzioni? 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 class MyClass<T> { T[] contents = new T[100]; // Non compila • Ecco perché: 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 public static <T extends Comparable<T>> T max(T max(List <T> coll) • 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 File LinkedList.java 019: 020: 021: 022: 023: 024: 025: 026: 027: 028: 029: 030: 031: 032: 033: 034: 035: /** Returns the first element in the linked list. @return the first element in the linked list */ public E getFirst() { if (first == null) throw new NoSuchElementException(); return first.data; } /** Removes the first element in the linked list. @return the removed element */ public E removeFirst() Continua { File LinkedList.java 036: 037: 038: 039: 040: 041: 042: 043: 044: 045: 046: 047: 048: 049: 052: 053: if (first == null) throw new NoSuchElementException(); E element = first.data; first = first.next; return element; } /** Adds an element to the front of the linked list. @param element the element to add */ public void addFirst(E element) { Node newNode = new Node(element,first); first = newNode; } Continua File LinkedList.java 054: 055: 056: 057: 058: 059: 060: 061: 062: 063: 064: 065: 066: 067: 068: 069: 070: 071: /** Returns an iterator for iterating through this list. */ public ListIterator<E> listIterator() { return new LinkedListIterator(); } private Node first; private class Node { E data; Node next; } 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: 073: 074: 075: 076: 077: 078: 079: 080: 081: 082: 083: private class LinkedListIterator implements ListIterator<E> { /** Costruisce un iteratore posizionato sul primo elemento della lista */ public LinkedListIterator() { last = null; // ultimo nodo visitato previous = null; // precedente a position } Continua LinkedListIterator – next() 084: 085: 086: 087: 088: 089: 090: 091: 092: 093: 094: 095: 096: 097: 098: 099: 100: 101: 102: /** Posizione l’iteratore sul prossimo. @return il prossimo elemento */ public E next() { if (!hasNext()) throw new NoSuchElementException(); previous = last; // Remember for remove if (last == null) last = first; else last = last.next; return last.data; } Continua LinkedListIterator – hasNext() 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: /** Testa se c’è un elemento dopo l’ultimo visitato. @return true se esiste un elemento dopo l’ultimo visitato */ public boolean hasNext() { if (last == null) return first != null; else return last.next != null; } Continua LinkedListIterator – add() 117: 118: 119: 121: 122: 123: 124: 125: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: /** Aggiunge un elemento dopo last e sposta l’iteratore sul prossimo elemento. */ public void add(E element) { if (last == null) { addFirst(element); last = first; } else { Node newNode = new Node(); newNode.data = element; newNode.next = last.next;// (1) last.next = newNode; // (2) last = newNode; // (3) } previous = last; // (4) Continua } LinkedListIterator – add() last LinkedListIterator – remove() 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: /** Rimuove l’elemento puntato da last. Può essere invocato solo dopo una chiamata a next() */ public void remove() { if (previous == last) throw new IllegalStateException(); if (last == first) { removeFirst(); } else { previous.next = last.next; // (1) } last = previous; // (2) } 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: 03: 04: 05: 06: 07: 08: 09: 10: 11: 12: 13: 14: 15: 16: 17: /** A list iterator allows access of a position in a linked list. This interface contains a subset of the methods of the standard java.util.ListIterator interface. The methods for backward traversal are not included. */ public interface ListIterator<E> { /** Moves the iterator past the next element. @return the traversed element */ E next(); /** Tests if there is an element after the iterator position. Continua File ListIterator.java 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: @return true if there is an element after the iterator position */ boolean hasNext(); /** Adds an element before the iterator position and moves the iterator past the inserted element. @param element the element to add */ void add(E element); /** Removes the last traversed element. This method may only be called after a call to the next() method. */ Continua File ListIterator.java 34: 35: 36: 37: 38: 39: 40: 41: 42: } void remove(); /** Sets the last traversed element to a different value. @param element the element to set */ void set(E element);