Il 0% ha trovato utile questo documento (0 voti)
7 visualizzazioni12 pagine

Generics

I Generics in Java permettono di scrivere codice generico che funziona con diversi tipi di dato, migliorando la robustezza e riducendo gli errori. Attraverso l'uso di parametri di tipo, come in una classe Optional<T>, si evita la necessità di downcast e si garantisce che solo oggetti del tipo corretto possano essere inseriti. Tuttavia, i Generics sono implementati tramite type erasure, il che significa che le informazioni sui parametri di tipo non esistono a runtime, portando a alcune limitazioni come l'impossibilità di creare array di tipi generici.

Caricato da

Entity reader
Copyright
© All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd
Il 0% ha trovato utile questo documento (0 voti)
7 visualizzazioni12 pagine

Generics

I Generics in Java permettono di scrivere codice generico che funziona con diversi tipi di dato, migliorando la robustezza e riducendo gli errori. Attraverso l'uso di parametri di tipo, come in una classe Optional<T>, si evita la necessità di downcast e si garantisce che solo oggetti del tipo corretto possano essere inseriti. Tuttavia, i Generics sono implementati tramite type erasure, il che significa che le informazioni sui parametri di tipo non esistono a runtime, portando a alcune limitazioni come l'impossibilità di creare array di tipi generici.

Caricato da

Entity reader
Copyright
© All Rights Reserved
Per noi i diritti sui contenuti sono una cosa seria. Se sospetti che questo contenuto sia tuo, rivendicalo qui.
Formati disponibili
Scarica in formato PDF, TXT o leggi online su Scribd

Introduzione ai Generics in Java

I Generics in Java sono un meccanismo del linguaggio per semplificare la scrittura di codice generico,
ovvero che funziona con più tipi di dato. In particolare, estende l’approccio basato sull’utilizzo di Object
che abbiamo visto in precedenza introducendo delle feature che rendono il codice più robusto e meno
soggetto a errori di utilizzo.

1 Motivazione
A titolo di esempio, consideriamo una classe chiamata Optional che realizza un contenitore che ospita
un singolo valore oppure è vuoto. Spesso si ricorre a null in questi casi. Tuttavia, il valore null è una
fonte costante di errori (NullPointerException) e non comunica chiaramente l’intenzione.
La classe Optional è quinde utile per gestire situazioni in cui un un valore può essere presente
oppure no.
Possiamo implementare un contenitore Optional per il tipo di dato String come segue:

public class OptionalString {


private String value;

public OptionalString() {
value = null;
}

public OptionalString(String value) {


[Link] = value;
}

public boolean isEmpty() {


return value == null;
}

public void replace(String newval) {


value = newval;
}

public String get() {


if (isEmpty()) {
throw new RuntimeException("no value in this Optional");
}
return value;
}

public String toString() {


if (!isEmpty()) {
return "Some(" + value + ")";
} else {
return "None";
}
}
}
Se volessimo applicare la stessa strategia alla gestione di variabili intere, ad esempio, dovremmo
scrivere una classe OptionalInteger, il cui codice sarebbe sostanzialmente identico a quello sopra, con
l’unica differenza che il tipo di dato di value è Integer invece di String. Questo andrebbe ripetuto per
ogni tipo di dato che vogliamo supportare. È un problema perchè duplicando il codice moltiplichiamo
la possibilità di fare errori.
Una soluzione, usando gli strumenti che abbiamo visto finora, è definire Optional in modo che la
variabile di istanza value sia di tipo Object: essendo questa la superclasse di tutte le classi in Java, il
contenitore funziona in tutti i casi.

public class OptionalObject {


private Object value;

public OptionalObject() {
value = null;
}

public OptionalObject(Object value) {


[Link] = value;
}

public boolean isEmpty() {


return value == null;
}

public void replace(Object newval) {


value = newval;
}

public Object get() {


if (isEmpty()) {
throw new RuntimeException("no value in this Optional");
}
return value;
}

public String toString() {


if (!isEmpty()) {
return "Some(" + value + ")";
} else {
return "None";
}
}
}

Il problema con questa versione è che, sebbene possiamo inserire qualunque valore all’interno di
OptionalObject, quando lo estraiamo con il metodo get dobbiamo fare un cast esplicito al tipo di dato,
usando quello corretto.

OptionalObject oo = new OptionalObject("Ciao ciao");


// Questa operazione funziona, perchè stiamo facendo
// il cast con la classe concreta corretta
String s = (String) [Link]();
Se usiamo il tipo di dato sbagliato per fare il cast non viene segnalato nessun errore al momento della
compilazione ma a runtime viene sollevata una ClassCastException, come nel seguente esempio.

OptionalObject oo = new OptionalObject("Ciao ciao");


// Questa operazione solleva una ClassCastException a runtime
Integer i = (Integer) [Link]();

In maniera ancora più subdola, il codice di OptionalObject consente di scrivere il seguente esempio
senza errori di compilazione.

// Creiamo un OptionalObject contenente una stringa


OptionalObject oo = new OptionalObject("Ciao ciao");
// Sostituiamo la stringa con un intero
[Link](1234);
// Cerchiamo di estrarre l'oggetto contenuto, ma facciamo il cast al tipo del
// valore originario. Dal momento che questo valore è cambiato anche come tipo,
viene
// sollevato un ClassCastException a runtime.
String s = (String) [Link]();

Riassumendo, OptionalObject:
• Consente di eliminare la duplicazione del codice, utilizzando una sola classe per gestire valori
optionali per tutti i tipi di dato in Java
• Inserendo un oggetto all’interno del contenitore OptionalObject questo viene automaticamente
convertito1 a Object.
• Quando estraiamo il valore, dobbiamo ricordarci di fare il downcast al tipo di dato corretto, pena
una ClassCastException.

2 Una soluzione: i Generics


I generics permettono di parametrizzare un tipo con uno o più parametri formali di tipo. Sono un
meccanismo di parametric polymorphism.

2.1 Sintassi dei Generics


La dichiarazione generica di una classe segue questa forma:

class NomeClasse<T> { ... }

Viene quindi utilizzata la consueta modalità di dichiarazione della classe, facendo seguire al nome della
classe il type parameter (parametro di tipo) <T>. Tipicamente si utilizza la lettera T, ma si può usare
qualunque lettera maiuscola.
Il parametro di tipo T funge da segnaposto per un tipo di dato che non è conosciuto al momento
della scrittura della classe. Possiamo quindi utilizzarlo nella dichiarazione di variabili di istanza, ad
esempio:

1Come sappiamo, l’oggetto non viene convertito, viene semplicemente reinterpretato il tipo di dato del riferimento.
class NomeClasse<T> {
private T variable;
}

Il tipo T viene sostituito da un tipo concreto al momento della istanziazione di oggetti della classe, la
quale avviene con una sintassi del tutto simile a quella vista sopra:

NomeClasse<String> varName = ...

Si noti che String ha preso il posto di T.

2.2 Esempio: Applicazione alla classe Optional


Possiamo usare il meccanismo dei Generics per al compilatore Java il compito di fare il cast al tipo di
dato appropriato quando estraiamo un valore con get e al contempo controllare che solo oggetti del
tipo previsto vengano inseriti nella struttura dati.

public class Optional<T> {


private T value;

public Optional() {
value = null;
}

public Optional(T value) {


[Link] = value;
}

public boolean isEmpty() {


return value == null;
}

public void replace(T newval) {


value = newval;
}

public T get() {
if (isEmpty()) {
throw new RuntimeException("no value in this Optional");
}
return value;
}

public String toString() {


if (!isEmpty()) {
return "Some(" + value + ")";
} else {
return "None";
}
}
}
Come notate il codice è sostanzialmente identico a quello di OptionalObject, con la differenza
dell’utilizzo del parametro di tipo T. Le conseguenze sono molteplici:
• il tipo della variabile di istanza value è T e non più object
• il costruttore e il metodo replace accettano solo valori di tipo T. La conseguenza è che una volta
istanziato un oggetto di tipo Optional possiamo rimpiazzare il valore contenuto solo con istanze
dello stesso tipo.
• il metodo get non restituisce più un Object, ma un riferimento di tipo T. Di conseguenza non serve
fare il cast del valore restituito dal metodo get.
A titolo di esempio:

Optional<String> os = new Optional<String>("ciao ciao");


// non serve più il downcast!
String s = [Link]();

Inoltre il seguente codice non compila più:

Optional<String> os = new Optional<String>("ciao ciao");


[Link](1234);

Il compilatore genera il seguente messaggio di errore per il codice riportato sopra:

[Link]: error: incompatible types: int cannot be converted to String


[Link](1234);

3 Metodi generici
Possiamo avere anche dei metodi generici indipendentemente dalla classe che li contiene, annotando
con il parametro di tipo (ovvero <T>) il metodo di interesse.
Ad esempio, potremmo aggiungere alla classe ArrayUtil, che abbiamo sviluppato in precedenza,
un metodo per scambiare due elementi di un array di tipo generico.

public class ArrayUtil {


public static <T> void swap(T[] array, int i, int j) {
T tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}

Notate che il parametro di tipo <T> precede il tipo di ritorno del metodo (in questo caso void).

4 Dietro le quinte: come Java implementa i generics


In questa sezione analizziamo come Java implementa i generics “dietro le quinte”. È un punto concet­
tualmente delicato ma fondamentale per capire perché i generics funzionano come funzionano e quali
limitazioni impongono.
Il concetto centrale è il seguente:
In Java, le informazioni sui parametri di tipo non esistono a runtime.

I generics sono una funzionalità del compilatore, non dell’interprete JVM. Durante la compilazione, il
compilatore svolge questi passi:
1. verifica la correttezza dei tipi (type checking);
2. rimuove i parametri di tipo;
3. inserisce i cast necessari in modo automatico.
Questo processo prende il nome di type erasure (cancellazione del tipo).
Ad esempio consideriamo una versione ridotta di Optional.
Prima della compilazione Dopo la compilazione (concettualmente)

public class Optional<T> { public class Optional {


private T value; private Object value;

public Optional(T value) { public Optional(Object value) {


[Link] = value; [Link] = value;
} }

public T get() { public Object get() {


return value; return value;
} }
} }

Utilizzo, prima della compilazione Utilizzo, dopo la compilazione (concettualmente)

Optional<String> os = new Optional os = new Optional("ciao");


Optional<String>("ciao"); String s = (String) [Link]();
String s = [Link]();

Quindi il cast che facevamo a mano nel primo esempio con OptionalObject non è scomparso: sempli­
cemente è stato spostato dal programmatore al compilatore.
La scelta della type erasure è storica e pragmatica. Ha permesso di:
• mantenere retrocompatibilità con codice Java precedente ai generics (Java 1.4 e precedenti);
• evitare modifiche profonde alla JVM;
• permettere a codice generico e non generico di coesistere.
Questo significa che:
• una Optional<String> e una Optional<Integer> sono la stessa classe a runtime;
• la distinzione è valida solo per il compilatore.
4.1 Conseguenze e limitazioni

4.1.1 Impossibilità di istanziare il parametro di tipo

public class Optional<T> {


public T create() {
return new T(); // ERRORE
}
}

Motivo: a runtime T non esiste, quindi la JVM non sa quale costruttore chiamare.

4.1.2 Impossibilità di creare array di tipi generici

T[] array = new T[10]; // ERRORE

Perché gli array conoscono il tipo a runtime, mentre i generics no. Questa asimmetria renderebbe il
sistema di tipi insicuro.

4.1.3 instanceof con parametri di tipo

if (obj instanceof Optional<String>) { // ERRORE


}

È consentito solo:

if (obj instanceof Optional) {


}

Poiché a runtime non esiste informazione su String come argomento di tipo.

4.1.4 Overloading e type erasure


La type erasure può causare conflitti di firma:

public void print(Optional<String> opt) { }


public void print(Optional<Integer> opt) { } // ERRORE

Dopo l’erasure infatti entrambe diventano:

public void print(Optional opt)

Il compilatore segnala correttamente l’ambiguità.

5 Interfacce e generics: l’esempio di Comparable<T>


Abbiamo visto che una classe può implementare l’interfaccia Comparable della libreria standard di Java
per rendere possibile il confronto tra differenti istanze della classe. In particolare, abbiamo visto che
possiamo implementare l’interfaccia Comparable per la classe BankAccount nel seguente modo:
public class BankAccount implements Comparable {
private double balance;

// Altri metodi e costruttori

public int compareTo(Object o) {


BankAccount other = (BankAccount) o;
if ([Link] < [Link]) {
return -1;
} else if ([Link] > [Link]) {
return 1;
} else {
return 0;
}
}
}

In questo codice, la firma di compareTo prevede di accettare un parametro di tipo Object. Questo ci
costringe a fare un downcast a runtime per convertire il riferimento del parametro a un riferimento di
tipo BankAccount, in modo da poter accedere alla variabile di istanza balance. Tuttavia, se l’oggetto
passato come parametro al metodo compareTo non è di tipo BankAccount, a runtime viene sollevata
una ClassCastException.
Leggendo con più attenzione la documentazione dell’interfaccia Comparable scopriamo tuttavia
che essa utilizza i generics! La dichiarazione dell’interfaccia è infatti la seguente:

public interface Comparable<T> {


public int compareTo(T other);
}

Notate che il parametro accettato da compareTo è di tipo T. Nell’esempio e nelle lezioni precedenti
abbiamo sfruttato il fatto che quando il parametro di tipo generico viene omesso il default è Object.
Ovvero, la dichiarazione

public class BankAccount implements Comparable

è equivalente a

public class BankAccount implements Comparable<Object>

Avendo a disposizione i generics noi possiamo dichiarare che BankAccount implementa Comparable
restringendo la possibilità di confronto solo a altre istanze di BankAccount:
public class BankAccount implements Comparable<BankAccount> {
private double balance;

// Altri metodi e costruttori

public int compareTo(BankAccount other) {


if ([Link] < [Link]) {
return -1;
} else if ([Link] > [Link]) {
return 1;
} else {
return 0;
}
}
}

Notate che con l’aggiunta del parametro di tipo a Comparable la firma di compareTo ora accetta solo
istanze di BankAccount. Questo consente di elminare il downcast a runtime e di catturare eventuali
errori di tipo al momento della compilazione.

6 Bounded Generics, o vincoli sui parametri di tipo


Dal momento che i generics sono implementati tramite type erasure, nell’esempio che abbiamo fatto
con Optional possiamo usare solo i metodi di Object all’interno dei metodi di Optional. Questo perchè
la variabile di istanza value è a tutti gli effetti di tipo Object a runtime2.
A volte tuttavia abbiamo bisogno di accedere a dei metodi più specifici di quelli di Object. Per
rendere l’idea più concreta, consideriamo la progettazione di una classe MinimumAccumulator che
implementa un contenitore in grado di memorizzare al più un elemento, aggiornabile nel tempo. Il
comportamento del contenitore è il seguente: ogni volta che viene fornito un nuovo valore tramite un
metodo update, MinimumAccumulator confronta il valore già memorizzato con quello nuovo e conserva
il minimo tra i due. In questo modo, il contenitore mantiene sempre traccia del valore minimo osservato
fino a quel momento.
Nell’implementazione del metodo update abbiamo quindi bisogno di confrontare l’oggetto corrente
e l’oggetto precedente. In altre parole abbiamo bisogno che il tipo di entrambi gli oggetti implementi
l’interfaccia Comparable<T>.
In Java possiamo specificare questo vincolo sul parametro di tipo con la seguente sintassi

<T extends Comparable<T>>

Notate che in questo contesto si usa la parola chiave extends anche se Comparable<T> è un’interfaccia.
Come abbiamo visto nella sezione precedente, l’utilizzo di Comparable<T> restringe la possibiltà di
confronto solo ad altre istanze della classe che sostituirà il segnaposto T.
Il seguente codice riporta l’esempio completo della classe MinimumAccumulator. Poiché, al
momento dell’istanziazione di un oggetto di questa classe, non è ancora stato fornito alcun valore

2Questa è una differenza fondamentale con C++, per chi lo conosce. In C++ infatti i template (che svolgono un
ruolo simile ai generics in Java) sono implementati tramite monomorphization: vengono generate «al volo» dal compi­
latore delle classi concrete con il parametro di tipo rimpiazzato dall’argomento di tipo concreto fornito al momento
dell’istanziazione. (Questa nota non è parte del programma d’esame).
da cui ricavare un minimo, utilizziamo la classe Optional definita in precedenza per rappresentare
esplicitamente il fatto che la variabile di istanza min potrebbe non contenere alcun valore. In questo
modo evitiamo l’uso di null e rendiamo il significato dello stato iniziale dell’oggetto chiaro e seman­
ticamente esplicito. Si noti che nel metodo update viene usato il metodo compareTo dell’interfaccia
Comparable<T>. Questo è possibile perchè tramite il vincolo sul parametro di tipo T il compilatore sa
che quel metodo sarà disponibile a runtime.

public class MinimumAccumulator<T extends Comparable<T>> {


private Optional<T> min;

public MinimumAccumulator() {
min = new Optional<T>();
}

public Optional<T> getMin() {


return min;
}

public void update(T newval) {


if ([Link]() || [Link]([Link]()) < 0) {
[Link](newval);
}
}
}

A tutti gli effetti, dopo la tipe erasure è come se nel codice sorgente T fosse sostituito da Comparable.
7 Esempio: uno Stack generico
In questo esempio vediamo una struttura dati leggermente più complessa, lo Stack. Abbiamo già visto
questa struttura dati utilizzando un array di Object come storage dei dati. Ora vediamo la versione che
utilizza i generics che consente di:
• evitare cast manuali quando invochiamo il metodo pop
• evitare che il metodo push accetti valori di tipi indesiderati
Come abbiamo discusso precedentemente, a causa della type erasure non possiamo istanziare un array
del tipo generico T, perchè a runtime questa informazione è scomparsa. La soluzione è quindi quella
adottata nel costruttore di questa classe: istanziare un array di Object e fare immediatamente il cast a
un array di riferimenti di tipo T.3

public class Stack<T> {


private T[] data;
private int size;

public Stack() {
// Attenzione: non possiamo creare direttamente un array di tipo `T`
[Link] = (T[]) new Object[4];
[Link] = 0;
}

public void push(T element) {


if (size == [Link]) {
// Attenzione: non possiamo creare direttamente un array di tipo `T`
T[] newdata = (T[]) new Object[[Link] * 2];
[Link](data, 0, newdata, 0, size);
data = newdata;
}
data[size] = element;
size++;
}

public T pop() {
if (isEmpty()) {
throw new RuntimeException("empty stack");
}

T ret = data[size - 1];


size--;
return ret;
}

public boolean isEmpty() {


return size == 0;
}

public int size() {


return size;
}
}

3È vero che stiamo facendo un cast manuale, che è uno dei motivi per cui abbiamo introdotto l’utilizzo dei generics.
Tuttavia questo cast è in un punto ben specifico del codice e non in ogni riga di codice che invoca il metodo pop.
8 Conclusione
I Generics risolvono i problemi introdotti dall’uso di Object per scrivere codice generico:

Prima dei Generics: Con i Generics:


• cast manuali • nessun cast
• errori a runtime • errori a compile-time
• codice poco leggibile • codice chiaro e sicuro
• nessuna garanzia sui tipi • controllo statico dei tipi

I generics sono fondamentali per comprendere:


• Java Collections Framework
• programmazione generica
• progettazione di API sicure

Potrebbero piacerti anche