670 likes | 915 Views
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.
E N D
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(); }
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: • 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();
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 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 ≤ BNON implica C<A> ≤ C<B> Set<Integer>NON è sottotipo di Set<Object> Continua
Generics e sottotipi • In generale, dati due tipi A e B , ed tipo generico C<T> abbiamo che: • MA … A ≤ BNON 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 leCollections • 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 • L’ultima istruzione invocherebbe intValue() sul primo elemento di ci • ma quell’elemento ha tipo String … • Il compilatore previene l’errore, rigettando la add() Collection<Integer> ci = new ArrayList<Integer>; Colletion<?> c = ci; c.add(“a string”); // non compila ci.get(0).intValue();
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() • Solito problema:drawAll() non può essere invocato su unaList<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 diShape • 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 } }