Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Dal linguaggio C al linguaggio Java parte 4, Dispense di Programmazione Java

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

Tipologia: Dispense

2019/2020

Caricato il 09/03/2020

libertar
libertar 🇮🇹

4.5

(15)

18 documenti

1 / 34

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
Dal linguaggio C al linguaggio Java
(Quarta parte)
Riccardo Silvestri
11-5-2009 17- 5-2010
Sommario della quarta parte
Genericità
Boxing e unboxing
Metodi generici
Esercizi Errori_MG_1 Errori_MG_2 Elementi_comuni Valore_comune Stampa_matrici Sostituzione
Tipi generici Interfacce generiche
Esercizi Errori_TG_1 Errori_TG_2 Punti_generici Copia_e_scambia Minmax Conta_valore
Insiemi_generici Multinsieme Conta_valori Code_generiche
Wildcards
Esercizi Errori_W_1 Errori_W_2 Componenti_comuni Elementi_differenti Intersezione Unione Max
Collezioni
Iterable e for- each Implementazione tramite classe nidificata statica for- each Implementazione tramite classe
locale
Esercizi Errori_I_1 Errori_I_2 Rimuovi_array Collection_comuni Max_ripetizioni Rimuovi_ripetizioni
Collection_liste
Il framework Collections Collection<E> Set<E> e HashSet<E> SortedSet<E> e TreeSet<E> List<E> e
ArrayList<E> Queue<E> e LinkedList<E> Map<K,V> e HashMap<K,V>
Esercizi Conta_ripetizioni Parole_frequenti ListToMap Lista_di_liste Threshold Anagrammi_in_file Grafi
Cammino_minimo
Genericità
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
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22

Anteprima parziale del testo

Scarica Dal linguaggio C al linguaggio Java parte 4 e più Dispense in PDF di Programmazione Java solo su Docsity!

Dal linguaggio C al linguaggio Java

(Quarta parte)

Riccardo Silvestri

Sommario della quarta parte

Genericità

Boxing e unboxing

Metodi generici

Esercizi Errori_MG_1 Errori_MG_2 Elementi_comuni Valore_comune Stampa_matrici Sostituzione

Tipi generici Interfacce generiche

Esercizi Errori_TG_1 Errori_TG_2 Punti_generici Copia_e_scambia Minmax Conta_valore

Insiemi_generici Multinsieme Conta_valori Code_generiche

Wildcards

Esercizi Errori_W_1 Errori_W_2 Componenti_comuni Elementi_differenti Intersezione Unione Max

Collezioni

Iterable e for-each Implementazione tramite classe nidificata statica for-each Implementazione tramite classe

locale

Esercizi Errori_I_1 Errori_I_2 Rimuovi_array Collection_comuni Max_ripetizioni Rimuovi_ripetizioni

Collection_liste

Il framework Collections Collection Set e HashSet SortedSet e TreeSet List e

ArrayList Queue e LinkedList Map<K,V> e HashMap<K,V>

Esercizi Conta_ripetizioni Parole_frequenti ListToMap Lista_di_liste Threshold Anagrammi_in_file Grafi

Cammino_minimo

Genericità

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 static int 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 static void 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 Pair extends 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 :

rappresenta un qualsiasi tipo che è un sottotipo di 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 static void 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 static void 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 static void copy2(DynamicArray dst, DynamicArray<? extends T> src) { // omessa perché uguale a quella del metodo copy() } public static void copy3(DynamicArray<? super T> dst, DynamicArray src) { // omessa perché uguale a quella del metodo copy() } public static void 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 objDA = new DynamicArray(); DynamicArray numDA = new DynamicArray(); DynamicArray intDA = new DynamicArray(); ... copy(objDA, intDA); // T è inferito essere Integer copy2(objDA, intDA); // T è inferito essere Object copy3(objDA, intDA); // T è inferito essere Integer copy4(objDA, intDA); // ERRORE in compilazione copy(intDA, objDA); // ERRORE in compilazione Test.copy(objDA, intDA); Test.copy(objDA, intDA); Test.copy(objDA, intDA); Test.copy2(objDA, intDA); Test.copy2(objDA, intDA); // ERRORE in compilazione Test.copy2(objDA, intDA); // ERRORE in compilazione Test.copy3(objDA, intDA); // ERRORE in compilazione Test.copy3(objDA, intDA); // ERRORE in compilazione Test.copy3(objDA, intDA); Test.copy4(objDA, intDA); // ERRORE in compilazione Test.copy4(objDA, intDA); // ERRORE in compilazione Test.copy4(objDA, intDA); // ERRORE in compilazione

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 e for-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 ACollection implements 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 Itr implements 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.