introduzione alla programmazione orientata agli oggetti e al
linguaggio Java. Si assume che il lettore conosca il linguaggio C cosicchè le caratteristiche di Java che
derivano direttamente dal C sono trattate il più rapidamente possibile
La genericità in Java può essere sinteticamente descritta come uno strumento per trattare i tipi in modo
parametrico. L'aggettivo generico è proprio usato per denotare un tipo o un metodo la cui definizione
dipende da una o più variabili di tipo. In questo modo la definizione del tipo o metodo è generica perchè
può essere adattata semplicemente sostituendo le variabili di tipo con tipi specifici. L'istanziamento delle
variabili di tipo, come vedremo, può essere esplicitamente specificata dal programmatore o è
automaticamente inferita dal compilatore. Tutto ciò ha un grande vantaggio. Permette di trattare in modo
uniforme e sintetico tipi e metodi che altrimenti sarebbero dovuti essere specificatamente definiti in
tantissime versioni tutte molto simili tra loro. E questo a sua volta migliora la leggibilità, la riusabilità e
facilita la manutenzione del codice.
Inizieremo descrivendo le conversioni boxing/unboxing che sono una agevolazione fornita dal
compilatore per sfruttare al meglio i benefici della genericità anche per i tipi primitivi. Poi passeremo a
discutere i metodi generici e infine i tipi generici.
Boxing e unboxing
In Java, come sappiamo, c'è una netta distinzione tra i tipi primitivi e i tipi riferimento. Il tipo Object è
un supertipo di tutti i tipi riferimento, siano essi classi, array o interfacce, mentre i tipi primitivi non
hanno supertipi. Il tipo Object permette di trattare oggetti di tipo differente in modo uniforme. Ad
esempio è possibile definire una classe per insiemi di Object che può essere usata per insiemi di oggetti
di un qualsiasi tipo riferimento. Ciò non è possibile per i tipi primitivi perché né Object né nessun'altro
tipo è un supertipo di tutti i tipi primitivi. Inoltre, come vedremo presto, anche la genericità non è
direttamente applicabile ai tipi primitivi.
Chiaramente, c'è un modo per sfruttare, almeno indirettamente, l'uniformità offerta dalla classe Object
e dalla genericità anche per i tipi primitivi: basterà introdurre per ogni tipo primitivo un corrispondente
tipo riferimento e usare quest'ultimo al posto del tipo primitivo. Proprio per agevolare questa possibilità,
nel package java.lang, ci sono otto tipi riferimento che corrispondono agli otto tipi primitivi in accordo
alla seguente tabella:
Tipo primitivo Tipo riferimento
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character
Inoltre, cosa ancora più importante, il compilatore Java esegue conversioni automatiche dai tipi primitivi
a questi tipi riferimento e viceversa, ovunque ciò risulti appropriato. La conversione di un tipo primitivo
al corrispondente tipo riferimento è chiamata boxing , mentre quella inversa, dal tipo riferimento al
corrispondente tipo primitivo, è chiamata unboxing. Ecco alcuni esempi:
Integer intO; intO = 13; // conversione boxing: è equivalente a intO = new Integer(13); // questo int k; k = intO; // conversione unboxing: è equivalente a k = intO.intValue(); // questo k = intOintO; // unboxing intO = kintO*(2 + intO); // unboxing e boxing Character charO = 'A'; // boxing char c = charO; // unboxing
Questi esempi riguardano int e char ma conversioni analoghe sono effettuate per tutti i tipi primitivi.
Per chiarire le conversioni relativamente agli array consideriamo i seguenti metodi:
public static int sum(Integer[] a) { int s = 0; for ( int i = 0 ; i < a.length ; i++) s += a[i]; // unboxing return s; } public static Integer sumInteger(Integer[] a) { return sum(a); // boxing }
E poi consideriamo i seguenti frammenti di codice:
Integer[] integerA = new Integer[] {2, 10003, 13}; // boxing Integer intO = 13; int [] intA = new int [] { intO, 2*intO }; // unboxing intA = integerA; // ERRORE in compilazione: tipi incompatibili (Integer[] e int[]) integerA = intA; // ERRORE in compilazione: tipi incompatibili (Integer[] e int[]) int k = sum(integerA); k = sumInteger(integerA); k = sum(intA); // ERRORE in compilazione: tipi incompatibili (Integer[] e int[])
Come si vede, le conversioni non si estendono agli array di tipi primitivi o dei loro corrispondenti tipi
Metodi generici
Un metodo generico è un metodo in cui uno o più nomi di tipo sono sostituiti da dei parametri formali
di tipo ( formal type parameters ) detti anche variabili di tipo ( type variables ). Quando il metodo è
invocato le variabili di tipo sono sostituite con nomi di tipi specifici, secondo certe regole che vedremo
fra poco. Le variabili di tipo devono essere dichiarate tra parentesi angolari, <...>, nell'intestazione del
metodo subito dopo gli eventuali modificatori e prima del tipo ritornato dal metodo.
Consideriamo un metodo find() che preso in input un array e un valore (dello stesso tipo delle
componenti dell'array) ritorna l'indice della prima posizione dell'array che contiene il valore e se il valore
non è presente ritorna -1. Volendo definire il metodo così che possa essere usato per array di un qualsiasi
tipo riferimento, lo definiamo generico rispetto al tipo delle componenti dell'array. Definiamo anche, a
mo' di confronto, una versione find2() che cerca di emulare la genericità usando il tipo Object. Inoltre
definiamo anche un main() per mettere alla prova i due metodi.
public class Test { // metodo generico: T è la variabile di tipo public staticint find(T[] a, T x) { for ( int i = 0 ; i < a.length ; i++) if (a[i].equals(x)) return i; return -1; } public static int find2(Object[] a, Object x) { for ( int i = 0 ; i < a.length ; i++) if (a[i].equals(x)) return i; return -1; } public static void main(String[] args) { String[] sA = new String[] {"A", "B", "C"}; Integer[] intA = new Integer[] {12, 23, 1, 234}; int k; k = find(sA, "C"); // T è sostituita con String k = find(intA, 2); // T è sostituita con Integer k = find(intA, "A"); // le stesse invocazioni sono possibili anche con il metodo find2() k = find2(sA, "C"); k = find2(intA, 2); k = find2(intA, "A"); } }
La variabile di tipo T è usata come se fosse il nome di un tipo effettivo. La differenza è che non ci sono
tipi effettivi che si chiamano T e T è infatti dichiarata variabile di tipo tramite l'espressione . In realtà
non è sempre vero che una variabile di tipo può essere usata come se fosse il nome di un tipo effettivo, ci
sono infatti alcune importanti eccezioni che vedremo fra poco.
Quando il metodo generico find() è invocato il compilatore inferisce il tipo che deve essere
"assegnato" alla variabile T. Si ricordi che una variabile di tipo può stare solamente per tipi riferimento,
non può mai essere sostituita con tipi primitivi (anche per questo sono state introdotte le conversioni
boxing/unboxing). Nella prima invocazione, siccome sia il tipo delle componenti dell'array del primo
argomento che il tipo del secondo argomento è String, il tipo inferito è String. Nella seconda
invocazione avviene una conversione boxing che converte il secondo argomento in un oggetto di tipo
Integer che è anche il tipo delle componenti dell'array passato come primo argomento. Ne deriva che il
tipo inferito è Integer. Nella terza invocazione i tipi relativi ai due argomenti sono differenti, il primo è
Integer e l'altro è String. In questi casi il tipo inferito dal compilatore è il supertipo comune più
"vicino" ai tipi relativi agli argomenti (in questo caso il tipo inferito è Serializable & Comparable<?
Le stesse invocazioni sono anche lecite per find2(). Allora, qual'è il vantaggio dell'uso della
genericità? La versione generica del metodo permette di stabilire il tipo da "assegnare" alla variabile T al
momento dell'invocazione. Il tipo da assegnare deve essere dichiarato tra parentesi angolari prima del
nome del metodo. Però in questo caso è necessario che l'invocazione del metodo sia qualificata
appropriatamente tramite il nome della classe per metodi statici e con this o super per metodi non
statici. Ecco alcuni esempi:
k = Test.find(intA, "A"); // ERRORE in compilazione
k = Test.find(intA, "C"); // ERRORE in compilazione k = Test.find(intA, 2); // OK
In questo modo è come se avessimo definito tantissime versioni (non generiche) del metodo find(), una
per il tipo String, una per il tipo Integer, e così via per ogni possibile tipo riferimento. Usato in questo
modo il metodo generico permette di ottenere la massima generalità, come la versione che usa Object,
mantenendo però il controllo statico del tipo. Così l'incongruità di una invocazione come find(intA,
"A"), che cerca una stringa in un array di interi, viene rilevata durante la compilazione. Si osservi che
questo tipo di errore potrebbe essere molto difficile da rilevare durante l'esecuzione perché non provoca
nessun lancio di eccezioni.
Quindi la genericità permette di scrivere un'unica versione generica di un metodo che sta per tutte le
versioni che potrebbero essere scritte per tutti i tipi riferimento che possono essere sostituiti alle variabili
di tipo. Inoltre il codice prodotto dal compilatore non è inutilmente gonfiato perchè esiste un'unica
versione compilata del metodo generico e questa è essenzialmente la stessa che sarebbe stata prodotta
scrivendo il metodo con il tipo Object sostituito alle variabili di tipo (con l'aggiunta in certi casi di
opportuni cast).
Consideriamo ora un'altro esempio: un metodo che preso in input un array e un valore assegna a tutte le
componenti dell'array il valore dato. Anche in questo caso definiamo sia la versione generica che quella
che usa il tipo Object.
public class Test { public staticvoid fill(T[] a, T x) { for ( int i = 0 ; i < a.length ; i++) a[i] = x; } public static void fill2(Object[] a, Object x) { for ( int i = 0 ; i < a.length ; i++) a[i] = x; } public static void main(String[] args) { String[] sA = ... Integer[] intA = ... fill(intA, 13); fill2(intA, 13); fill(intA, "A"); // ERRORE in esecuzione: ArrayStoreException fill2(intA, "A"); // ERRORE in esecuzione: ArrayStoreException Test.fill(intA, "A"); // ERRORE in compilazione fill(sA, "A"); fill(sA, 12); // ERRORE in esecuzione: ArrayStoreException Test.fill(sA, 12); // ERRORE in compilazione Test.fill(sA, "A"); } }
Una invocazione incongrua come fill(intA, "A") provoca un errore in esecuzione con lancio
dell'eccezione ArrayStoreException perché si è tentato di assegnare un valore di tipo String a una
componente di un array di Integer. Se si usa l'invocazione Test.fill(intA, "A") l'errore è
rilevato in compilazione. A differenza della versione fill2() del metodo per cui non c'è nessun modo
per far sì che lo stesso errore sia rilevabile in compilazione.
Il prossimo esempio mostra che la genericità permette di evitare (o perlomeno limitare) l'uso di cast
che potrebbero fallire in esecuzione. Il metodo getMiddle() semplicemente ritorna il valore che si trova
nella posizione centrale dell'array di input. Il metodo longestString() ritorna il valore dell'array di
input con la massima lunghezza della stringa data dal metodo toString().
public class Test { public static T getMiddle(T[] a) { return a[a.length/2]; } public static Object getMiddle2(Object[] a) { return a[a.length/2]; } public static T longestString(T[] a) { T val = null ;
Questa tecnica di usare clone() non va bene se il nuovo array deve avere uan lunghezza maggiore
rispetto a quello dell'array di input. In questi casi si possono usare altre tecniche che vedremo più avanti.
Negli esempi che abbiamo visto finora i metodi generici sono statici e una sola variabile di tipo è usata.
In generale, come vedremo a breve, si possono definire metodi generici non statici e si possono usare
due o più variabili di tipo. Per quanto riguarda i nomi delle variabili di tipo non ci sono restrizioni
particolari, seguono le stesse regole di un qualsiasi altro nome di variabile. Però, per meglio distinguerle
da nomi di variabili e nomi di tipi, è consuetudine che i loro nomi consistano in singole lettere maiuscole
(T, S, ecc.).
Esercizi
[Errori_MG_1] Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In
particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.
public class Test { public static T first(T[] A, T[] B) { int i = 0; while (i < A.length && i < B.length && !A[i].equals(B[i])) i++; return (i < A.length && i < B.length? A[i] : null); } public static void main(String[] args) { long [] longA = {2, new Long(5)}; int [] intA = {1, 2, 3}; int val = first(longA, longA); val = first(longA, intA); Integer[] intA2 = {2, 3, 4}; Long[] longA2 = {1L, 2L, 3L}; val = first(intA2, longA2); Long vL = first(intA2, longA2); Number num = first(intA2, longA2); } }
[Errori_MG_2] Il seguente programma contiene uno o più errori. Trovare gli errori e spiegarli. In
particolare, dire per ogni errore se si verifica in compilazione o durante l'esecuzione.
public class Test { public static E sub(E[] a, E v) { int i = 0; while (i < a.length && !v.toString().equals(a[i].toString())) i++; if (i < a.length) { E c = a[i]; a[i] = v; return c; } else return null ; } public static void main(String[] args) { String[] sA = {"A", "B", "C"}; String s = sub(sA, "D"); Integer[] intA = {1, 2, 3}; int k = sub(intA, 13); k = sub(intA, "2"); Object obj = sub(intA, "3"); obj = Test.sub(intA, '4'); Object[] objA = {2, 3, 4}; Integer v = sub(objA, 2); } }
[Elementi_comuni] Scrivere un metodo generico che presi in input due array ritorna il numero di
elementi del primo array che sono presenti anche nel secondo array. Scrivere anche una versione non
generica usando il tipo Object. Confrontare le due versioni e discutere i vantaggi e i svantaggi dei loro
possibili usi.
[Valore_comune] Scrivere un metodo generico che presi in input due array ritorna il primo valore del
primo array che appare anche nel secondo. Se non ci sono valori in comune ritorna null. Scrivere anche
una versione non generica del metodo e discutere i vantaggi e i svantaggi dei possibili usi delle due
versioni.
[Stampa_matrici] Scrivere un metodo generico che presa in input una matrice la stampa in modo che
le colonne siano allineate come nei seguenti esempi:
MATRICE di Double MATRICE di String 0.123 23.5 12.01 Roma Milano Genova 1 0.4 1.234 Napoli Reggio Calabria Teramo 1234567 67.897 12 Terni Palermo Perugia
Scrivere anche una versione non generica e discutere i vantaggi e i svantaggi dei possibili usi delle due
versioni.
[Sostituzione] Scrivere un metodo generico che prende in input un array e due valori x e y, e crea e
ritorna una copia dell'array di input con tutti i valori x sostituiti dal valore y.
Tipi generici
Al pari dei metodi generici che aggiungono flessibilità al linguaggio senza sacrificare il controllo
statico sui tipi, Java supporta anche la definizione di tipi generici ( generic types ). Un tipo generico è una
classe o una interfaccia che nella sua dichiarazione ha una o più variabili di tipo (dichiarate tra parentesi
angolari). Una variabile di tipo è usata nella definizione della classe/interfaccia alla stregua di un
qualsiasi nome di tipo effettivo, con le stesse limitazioni viste per i metodi generici. Ogni tipo generico
definisce un insieme di tipi parametrici ( parameterized types ) che consistono nel nome della classe o
interfaccia seguito da una lista di nomi di tipi effettivi (tra parentesi angolari) corrispondenti alle
variabili di tipo. Consideriamo un semplice esempio di tipo generico che rappresenta una coppia di
valori:
public class Pair { private T first, second; public Pair(T first, T second) { this .first = first; this .second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public void setFirst(T x) { first = x; } public void setSecond(T x) { second = x; } }
La variabile di tipo T è dichiarata, come per i metodi generici, tra parentesi angolari e immediatamente
dopo il nome della classe (o dell'interfaccia). Poi, nella definizione della classe può essere usata come se
fosse il nome di un tipo effettivo (eccetto che nelle espressioni creazionali). In generale, per istanziare un
oggetto tramite un tipo generico è necessario specificare esplicitamente quali sono i tipi che devono
essere sostituiti alle corrispondenti variabili di tipo. In altre parole, è necessario specificare quale dei tipi
parametrici che possono corrispondere al tipo generico deve essere istanziato. Nel caso del tipo generico
Pair si deve specificare quale tipo effettivo (ad esempio, String, Double, ecc.) deve sostituire la
variabile T. Ecco alcuni esempi di uso di questo tipo generico:
Pair strPair = new Pair("primo", "secondo"); strPair.setFirst("I"); strPair.setSecond(2); // ERRORE in compilazione: Integer non è un sottotipo di String Pair intP = new Pair(13, 17); // boxing intP.setFirst(11); // boxing intP.setSecond("13"); // ERRORE in compilazione: String non è un sottotipo di Integer int k = intP.getFirst(); // unboxing String s = intP.getFirst(); // ERRORE in compilazione: Integer non è un sottotipo di String Pair<Pair> strPairPair = new Pair<Pair>(strPair, strPair);
Per istanziare una coppia di stringhe si usa il tipo parametrico Pair e per istanziare una coppia
di interi si usa Pair. L'ultima riga mostra che un tipo parametrico può a sua volta essere usato
L'assegnamento pntPair = lpPair provoca un errore in compilazione perché il tipo di lpPair, che è
Pair, non è un sottotipo del tipo di pntPair, che è Pair. Questo può apparire
sorprendente ma se così non fosse allora sarebbero possibili le istruzioni (2) e (3). L'istruzione (3)
provocherebbe un errore in esecuzione perchè il primo elemento della coppia in lpPair è stato posto
dall'istruzione (2) uguale ad un oggetto di tipo Point il quale non ha il metodo getLabel(). Quindi se
Pair fosse trattato come un sottotipo di Pair la genericità non potrebbe garantire la
sua proprietà fondamentale: il controllo statico sui tipi. D'altronde se immaginiamo delle classi
PointPair e LPointPair che definiscono direttamente coppie di Point e coppie di LPoint non c'è
ragione perché debbano essere l'una il sottotipo dell'altra. In generale, quindi, se Type1 e Type2 sono due
qualsiasi tipi distinti allora Pair e Pair non hanno nessuna relazione di sottotipo o
supertipo fra loro. Questo è in contrasto con il comportamento degli array. Sappiamo che se Type1 è un
sottotipo di Type2 allora Type1[] è un sottotipo di Type2[]. Ma sappiamo anche che gli array non
permettono un controllo statico dei tipi:
LPoint[] lpA = new LPoint[3]; Point[] pA = lpA; // lecito perché LPoint[] è un sottotipo di Point[] pA[0] = new Point(0, 0); // ERRORE in esecuzione (non rilevabile dal compilatore)
Infatti un array, a differenza dei tipi parametrici, mantiene nel codice compilato il tipo delle componenti.
E deve essere così perchè altrimenti l'errore precedente non sarebbe rilevabile, neanche in esecuzione.
Questa è la ragione per cui non è possibile creare array le cui componenti sono di un tipo parametrico:
Pair[] strPairA; // OK strPairA = new Pair[3]; // ERRORE in compilazione
Però è possibile dichiarare variabili il cui tipo è un array di un tipo parametrico. Discuteremo più avanti il
perché di questa stranezza.
La classe generica Pair ha una sola variabile di tipo. Diamo ora un esempio che usa due variabili di
tipo. Si tratta di una versione più generale della classe Pair perché permette che i tipi delle due
componenti possano essere diversi.
public class DPair<F, S> { private F first; private S second; public DPair(F first, S second) { this .first = first; this .second = second; } public F getFirst() { return first; } public S getSecond() { return second; } public void setFirst(F x) { first = x; } public void setSecond(S x) { second = x; } }
Avremmo potuto definire prima questa classe generica e poi Pair come sottoclasse (generica):
public class Pairextends DPair<T, T> { public Pair(T first, T second) { super (first, second); } }
Se avessimo fatto così, per ogni tipo specifico Type, si avrebbe che
Pair è un sottotipo di DPair<Type, Type>.
D'altronde ciò è in accordo con l'intuizione che equipara Pair ad una classe non generica che è
definita estendendo una classe (non generica) che corrisponde a DPair<Type, Type>. Ovviamente
questo vale in generale.
Interfacce generiche Non solo le classi ma anche le interfacce possono essere generiche. È anzi più
facile incontrare esempi di interfacce generiche che di classi generiche e di solito le classi generiche
sono implementazioni di interfacce generiche. Per le interfacce valgono le stesse regole di dichiarazione
e uso delle variabili di tipo. Iniziamo considerando una delle interfacce generiche più implementate e
usate della libreria di Java. L'interfaccia Comparable, del package java.lang, serve ad imporre un
ordinamento totale degli oggetti della classe che la implementa:
public interface Comparable { int compareTo(T obj); }
Il metodo compareTo(T obj) deve ritornare un intero negativo se l'oggetto su cui è invocato è minore di
obj, un intero positivo se invece è maggiore di obj e zero se sono uguali. Tantissime classi della libreria
Java implementano questa interfaccia. Ad esempio, le otto classi che corrispondono ai tipi primitivi,
String, Date, ecc. Ovviamente, le classi implementano un opportuno tipo parametrico (o interfaccia
parametrica) che deriva dall'interfaccia generica Comparable. La classe String implementa
Comparable, la classe Integer implementa Comparable, Double implementa
Comparable e così via.
Modifichiamo la definizione della classe Point implementando l'interfaccia Comparable:
public class Point implements Comparable { private int x, y; public Point( int x, int y) { this .x = x; this .y = y; } public int getX() { return x; } public int getY() { return y; } public int compareTo(Point p) { if (p == null ) throw new NullPointerException(); int px = p.getX(), py = p.getY(); if (px == x && py == y) return 0; // this è uguale a p else if (px < x || (px == x && py < y)) return 1; // this è maggiore di p else return -1; // this è minore di p } }
L'ordinamento implementato è molto semplice: ordina rispetto alle ascisse e a parità di ascissa ordina
rispetto alle ordinate.
L'interfaccia generica Comparable permette di implementare metodi generici che si basano su una
relazione di ordinamento. Ad esempio, trovare il minimo di un array o un insieme, trovare il massimo,
ordinare un array, cercare un valore in un array tramite ricerca binaria, ecc. Proviamo allora a definire un
metodo generico che ritorna il valore minimo di un array:
public static T min(T[] a) { T min = a[0]; for ( int i = 1 ; i < a.length ; i++) if (a[i].compareTo(min) < 0) // ERRORE in compilazione: non trova compareTo() min = a[i]; return min; }
Questa implementazione non va bene perché il metodo compareTo() non è un metodo come quelli di
Object che appartengono a tutti i tipi riferimento e quindi a tutti i possibili tipi che la variabile T può
rappresentare. Affinché si possa scrivere un metodo generico come questo occorre che la variabile di tipo
sia limitata ai tipi T che implementano l'interfaccia Comparable. Ciò è possibile:
public static <T extends Comparable> T min(T[] a) { T min = a[0]; for ( int i = 1 ; i < a.length ; i++) if (a[i].compareTo(min) < 0) min = a[i]; return min; }
La dichiarazione <T extends Comparable> significa proprio che la variabile di tipo T varia
solamente tra i tipi che sono sottotipi di Comparable, cioè, i tipi che implementano tale interfaccia.
In altre parole, un tipo effettivo Type può sostituire la variabile di tipo T se e solo se Type implementa
l'interfaccia Comparable. Per chiarire bene questo punto consideriamo un frammento di codice
boolean r = below(a, 13); Integer[] intA = new Integer[] {1, 2, 3}; r = below(intA, 10); int [] iA = {1, 2, 3}; r = below(iA, 5); } }
[Punti_generici] Definire una versione generica GLPoint della classe LPoint in cui la label ha
tipo generico T. La classe GLPoint deve estendere la classe Point. Così LPoint corrisponderebbe a
GLPoint.
[Copia_e_scambia] Definire un metodo generico copySwap che presa in input una coppia di tipo
Pair, crea e ritorna una nuova coppia dello stesso tipo con i valori della coppia di input scambiati.
[Minmax] Scrivere un metodo generico che preso in input un array ritorna il valore minimo e il valore
massimo dell'array. Usare Pair per ritornare la coppia di valori.
[Conta_valore] Scrivere un metodo generico che preso in input un array ritorna il valore più frequente
e il numero di occorrenze (usare in modo opportuno il tipo DPair<F, S> per ritornare la coppia valore e
numero occorrenze). Ecco alcuni esempi di input e output del metodo:
INPUT OUTPUT
{"B", "AB", "A", "B"} ("B", 2)
[Insiemi_generici] Definire una classe GSet per rappresentare insiemi i cui elementi sono di tipo
(generico) T. Implementare dei metodi per le seguenti operazioni:
determinare se un dato oggetto è presente o meno nell'insieme;
aggiungere un oggetto all'insieme (se non è già presente);
rimuovere un oggetto dall'insieme.
[Multinsieme] Definire una classe generica MSet per rappresentare multi-insiemi i cui elementi
sono di tipo (generico) T. Un multi-insieme a differenza di un insieme può contenere uno stesso elemento
più volte. In altri termini ogni elemento appartenente al multi-insieme vi appartiene con una certa
molteplicità (1, 2, 3, ...). Implementare dei metodi per le seguenti operazioni:
determinare la molteplicità di un dato oggetto (se non è presente, la molteplicità è zero);
aggiungere un oggetto al multi-insieme (se è già presente, la molteplicità è incrementata di uno);
rimuovere un oggetto dal multi-insieme (se è presente, la molteplicità è decrementata di uno).
Suggerimento: rappresentare ogni elemento con una coppia (valore, molteplicità).
[Conta_valori] Scrivere un metodo generico che preso in input un array ritorna un MSet (vedi
l'esercizio [Multinsieme]) che rappresenta i valori presenti nell'array. Ecco un esempio:
ARRAY {"A", "AB", "B", "A", "AB", "AB"}
MULTI-INSIEME {("A", 2), ("AB", 3), ("B", 1)}
[Code_generiche] Definire una interfaccia generica per code e fornire delle implementazioni (tramite
classi generiche) con liste e con array.
Wildcards
Sappiamo che i tipi generici e più precisamente i tipi parametrici si comportano in modo diverso dagli
array per quanto riguarda le relazioni di sottotipo e, simmetricamente, di supertipo. Ricordiamo che se
TypeB è un sottotipo di TypeA allora TypeB[] è un sottotipo di TypeA[], mentre GenType non è
un sottotipo di GenType (a meno che TypeA non coincida con TypeB), dove GenType è un
qualsiasi tipo generico. Ciò è necessario affinché si possa garantire un controllo statico sui tipi
parametrici. Però in alcune situazioni, come nel caso del precedente metodo min(), sarebbe utile avere
un modo, dato un tipo Type, per riferirsi a un qualsiasi tipo parametrico GenType tale che SubT è
un sottotipo di Type, o anche a un qualsiasi tipo parametrico GenType tale che SuperT è un
supertipo di Type. Il linguaggio Java permette di fare ciò con la sintassi delle wildcards :
**extends** Type>
rappresenta un qualsiasi tipo che è un sottotipo di Type.
**super** Type>
rappresenta un qualsiasi tipo che è un supertipo di Type.
rappresenta un qualsiasi tipo che è un sottotipo di Object, quindi un qualsiasi tipo riferimento
senza restrizioni. È equivalente a <? extends Object>.
La wildcard è rappresentata dal carattere? e Type può essere un tipo effettivo, una variabile di tipo o un
tipo generico. La wildcard non è una variabile di tipo perché il suo scope è ristretto alle parentesi
angolari in cui è usata. Le espressioni con wildcards possono essere usate ovunque si può usare il nome
di un tipo, con alcune eccezioni che discuteremo fra poco.
Per alcuni esempi ci sarà utile la seguente classe generica che rappresenta array dinamici, cioè, array la
cui lunghezza può essere variata a piacimento:
import static java.util.Arrays.*; public class DynamicArray { private T[] array; public DynamicArray() { array = (T[]) new Object[0]; } public int getSize() { return array.length; } public void setSize( int size) { if (size < 0) throw new IllegalArgumentException(); array = copyOf(array, size); } public T get( int index) { if (index < 0 || index >= array.length) throw new IllegalArgumentException(); return array[index]; } public void set( int index, T x) { if (index < 0 || index >= array.length) throw new IllegalArgumentException(); array[index] = x; } }
Se ci occorresse un metodo generico che stampa un array dinamico potremmo scriverlo così:
public staticvoid printDynArray(DynamicArray a) { int size = a.getSize(); for ( int i = 0 ; i < size ; i++) System.out.println(a.get(i)); }
Però la variabile di tipo T è usata solamente per denotare il tipo dell'array dinamico. In questi casi può
essere sostituita con una wildcard:
public static void printDynArray(DynamicArray<?> a) { int size = a.getSize(); for ( int i = 0 ; i < size ; i++) System.out.println(a.get(i)); }
Questa versione è del tutto equivalente a quella che usa la variabile di tipo. In tutti i casi, come questo, in
cui una variabile di tipo è usata una sola volta (cioè, non ci sono dipendenze legate a tale variabile) si
preferisce sostituirla con una wildcard.
Supponiamo di voler aggiungere alla classe DynamicArray un metodo che prende in input un array
dinamico e ne appende i valori all'array dinamico dell'oggetto. Potremmo definirlo così:
public void append(DynamicArray a) { int size = a.getSize(); int oldSize = array.length; setSize(oldSize + size); for ( int i = 0 ; i < size ; i++) array[oldSize + i] = a.get(i); }
Vediamo ora altri esempi per chiarire meglio il significato e l'uso delle wildcards. Consideriamo quattro
versioni diverse di un metodo generico che copia un array dinamico in un'altro:
public class Test { public staticvoid copy(DynamicArray<? super T> dst, DynamicArray<? extends T> src) { int size = src.getSize(); if (dst.getSize() < size) dst.setSize(size); for ( int i = 0 ; i < size ; i++) dst.set(i, src.get(i)); } public staticvoid copy2(DynamicArray dst, DynamicArray<? extends T> src) { // omessa perché uguale a quella del metodo copy() } public staticvoid copy3(DynamicArray<? super T> dst, DynamicArray src) { // omessa perché uguale a quella del metodo copy() } public staticvoid copy4(DynamicArray dst, DynamicArray src) { // omessa perché uguale a quella del metodo copy() } }
Il seguente frammento di codice mostra le differenze tra le quattro versioni:
DynamicArray
Il metodo copy() è il più flessibile dei quattro mentre copy4() è il più restrittivo. Le versioni copy(),
copy2() e copy3() ammettono le stesse invocazioni quando il parametro di tipo è implicito (può variare
solamente il tipo che è inferito per la variabile di tipo T). Quando invece il parametro di tipo è dichiarato
esplicitamente anche le prime tre versioni si differenziano.
Consideriamo ora un metodo che calcola la somma dei valori di un array dinamico di numeri:
public class Test { public static double sum(DynamicArray<? extends Number> a) { double sum = 0; int n = a.getSize(); for ( int i = 0 ; i < n ; i++) sum += a.get(i).doubleValue(); // unboxing return sum; } }
Ed ecco come il metodo può essere usato:
DynamicArray ints = new DynamicArray(); DynamicArray floats = new DynamicArray(); DynamicArray nums = new DynamicArray();
DynamicArray strs = new DynamicArray(); ... double tot = sum(ints); tot = sum(floats); tot = sum(nums); tot = sum(strs); // ERRORE in compilazione
Quindi il metodo sum() accetta come input solamente gli array dinamici di tipi numerici, cioè, sottotipi di
Number.
Le espressioni che usano wildcard rappresentano dei tipi che sono chiamati tipi wildcard ( wildcard
types ). I tipi wildcard hanno relazioni di sottotipo/supertipo tra loro e con i tipi parametrici ordinari. Il
seguente diagramma mostra alcuni esempi di queste relazioni per wildcard con la limitazione extends :
| Pair<?> |
Δ
|
| |
| Pair | | Pair<? extends Number> |
Δ
|
| |
| Pair<? extends Integer> | | Pair |
Δ
|
| Pair |
Relazioni simmetriche valgono anche per wildcard con la limitazione super :
| Pair<?> |
Δ
|
| Pair<? super Integer> |
Δ
|
| |
| Pair | | Pair<? super Number> |
Δ
|
| |
| Pair | | Pair |
Vediamo ora alcuni esempi che mostrano le conseguenze di tali relazioni nella dichiarazione di variabili
e nell'assegnamento di valori a variabili:
DynamicArray> da = **new** DynamicArray>(); // ERRORE in compilazione DynamicArray<Pair>> pairDA = **new** DynamicArray>(); pairDA.setSize(10); pairDA.set(0, new Pair(1, 2)); pairDA.set(1, new Pair("a", "b")); Pair intPair = null ;
su cui è invocato ha il tipo delle componenti che è Point, allora il metodo deve accettare array dinamici
con tipo uguale a un qualsiasi sottotipo di Point, come LPoint.
[Elementi_differenti] Aggiungere alla classe DynamicArray un metodo che preso in input un array
dinamico appende ad esso tutti gli elementi dell'array dinamico dell'oggetto che non sono già presenti
nell'array di input. Il metodo deve essere flessibile. Se ad esempio l'array dinamico dell'oggetto su cui il
metodo è invocato ha il tipo degli elementi che è LPoint, allora il metodo deve accettare array dinamici
con tipo uguale a un qualsiasi supertipo di LPoint, come Point.
[Intersezione] Aggiungere alla classe DynamicArray un metodo che preso in input un array
dinamico crea e ritorna un nuovo array dinamico che contiene tutti gli elementi dell'array dinamico
dell'oggetto che sono uguali a elementi presenti nell'array di input. Come deve essere definito per avere
la massima flessibilità compatibilmente con un uso appropriato?
[Unione] Definire un metodo generico che prende in input due array dinamici e crea e ritorna un nuovo
array dinamico che contiene l'unione degli elementi dei due array. Il metodo dovrebbe essere il più
flessibile possibile.
[Max] Definire un metodo generico che preso in input un array dinamico ritorna il valore massimo
dell'array. Come al solito, massimizzare la flessibilità del metodo.
Collezioni
Nella programmazione si presenta frequentemente la necessità di gestire un insieme, una lista, una
coda, ovvero, un qualche tipo di collezione di oggetti. Proprio per venire incontro a questa diffusa
esigenza la piattaforma Java mette a disposizione dei programmatori il framework denominato
Collections , nel package java.util. Si tratta di un insieme ben organizzato di interfacce, classi
astratte e classi concrete che, anche grazie alla genericità, fornisce un valido aiuto in tutti i casi in cui c'è
bisogno di usare un qualche tipo di collezione di oggetti.
Iterable efor-each
Una interfaccia molto importante per quasi tutte le collezioni è Iterable, nel package java.lang:
public interface Iterable { // ritorna un iteratore per elementi di tipo E Iterator iterator(); }
Scopo dell'interfaccia Iterable è di stabilire una modalità standard per visitare (o scorrere) tutti gli
elementi di una collezione. L'unico metodo dell'interfaccia, iterator(), ritorna un riferimento ad un
oggetto di tipo Iterator. A sua volta Iterator è una interfaccia generica definita nel package
java.util:
public interface Iterator { // ritorna true se ci sono ancora elementi da visitare boolean hasNext(); // ritorna il prossimo elemento E next(); // rimuove l'elemento ritornato dall'ultima invocazione di next() void remove(); }
Il metodo next(), se invocato quando non ci sono più elementi da visitare, lancia un'eccezione di tipo
NoSuchElementException. Il metodo remove(), se invocato quando next() non è stato ancora invocato
o remove() è stato già invocato dopo l'ultima invocazione di next(), lancia un'eccezione di tipo
IllegalStateException. Inoltre, il metodo remove() è facoltativo, nel senso che una classe che
implementa l'interfaccia Iterator può anche non supportare il metodo (ad esempio, quando la collezione
non è modificabile). Se remove() non è supportato allora l'invocazione del metodo lancia un'eccezione di
tipo UnsupportedOperationException.
Conosciamo già una classe che implementa l'interfaccia Iterator ed è la classe Scanner che
implementa Iterator. Infatti, i metodi next() e hasNext() di Scanner implementano proprio
i metodi dell'interfaccia. Per ovvie ragioni il metodo remove() non è invece supportato. Una classe che
gestisce un qualche tipo di collezione implementa, generalmente, l'interfaccia più flessibile Iterable,
come fanno quasi tutte le classi del framework Collections. La ragione della maggiore flessibilità
dell'interfaccia Iterable sta nel permettere di usare più iteratori (cioè, oggetti di tipo Iterator)
simultaneamente. Così è possibile fare due (o più) scansioni nidificate ed indipendenti della collezione,
una con un iteratore e l'altra con un'altro iteratore. Ciò è necessario se, ad esempio, si vogliono
confrontare gli elementi a due a due. Questa flessibilità in più è anche il motivo per cui la classe
Scanner non implementa l'interfaccia Iterable.
Implementazione tramite classe nidificata statica Prima di vedere le principali classi del framework
Collections e quindi anche molti esempi di classi che implementano l'interfaccia Iterable, vediamo
come l'interfaccia può essere implementata e usata. Vogliamo quindi definire una semplice classe per
gestire collezioni di elementi che implementa l'interfaccia Iterable. Per implementare Iterable
useremo una classe nidificata statica che implementa l'interfaccia Iterator, così che un'istanza di tale
classe realizza un iteratore che può essere ritornato dal metodo iterator().
import java.util.; public class ACollectionimplements Iterable { private E[] array; //Array che mantiene gli elementi della collezione private int size; //Numero di elementi presenti nella collezione private int modCount = 0; //Contatore delle modifiche (serve all'iteratore) //Costruisce una collezione vuota public ACollection() { array = (E[]) new Object[0]; size = 0; } //Metodo ausiliario che ritorna il primo indice dell'array che contiene private int find(E x) { //l'elemento x, se non è presente ritorna -1. for ( int i = 0 ; i < array.length ; i++) if (x == null? array[i] == null : array[i].equals(x)) return i; return -1; } //Metodo ausiliario che rimuove l'elemento in posizione i private void remove( int i) { array[i] = array[--size]; modCount++; } //Ritorna true se l'elemento x è contenuto nella collezione public boolean contains(E x) { return (find(x) != -1); } //Aggiunge l'elemento x alla collezione (anche se è già presente) public void add(E x) { if (array.length == size) array = Arrays.copyOf(array, (3size)/2 + 1); array[size++] = x; modCount++; } //Aggiunge alla collezione tutti gli elementi passati come argomenti public void addAll(E...a) { for ( int i = 0 ; i < a.length ; i++) add(x); } //Rimuove dalla collezione la prima occorrenza dell'elemento x e ritorna true, public boolean remove(E x) { //se non è presente ritorna false. int i = find(x); if (i != -1) { remove(i); return true ; } else return false ; } //Ritorna il numero di elementi della collezione public int size() { return size; } //Classe nidificata statica che implementa l'iteratore private static class Itrimplements Iterator { private ACollection coll; //La collezione da iterare private int cursor = 0; //Indice del prossimo elemento private int lastRet = -1; //Indice dell'ultimo elemento ritornato, se non c'è -1.