Vai alla Home Page About me Courseware Federica Living Library Federica Federica Podstudio Virtual Campus 3D Le Miniguide all'orientamento Gli eBook di Federica La Corte in Rete
 
 
Il Corso Le lezioni del Corso La Cattedra
 
Materiali di approfondimento Risorse Web Il Podcast di questa lezione

Marco Faella » 17.L'implementazione della programmazione generica: la cancellazione (erasure)


La cancellazione

In questa lezione, verrà illustrato il meccanismo interno che rende disponibile la programmazione generica, e le limitazioni che questo meccanismo comporta sull’uso dei parametri di tipo.

Il meccanismo che supporta la programmazione generica prende il nome di cancellazione (in Inglese, erasure).
Il principio di funzionamento è il seguente:

  • I parametri di tipo vengono usati dal compilatore per effettuare i dovuti controlli di tipo (type checking);
  • Poi, tutti i parametri di tipo vengono rimossi (cancellati, appunto) e sostituiti da Object, oppure dal primo limite superiore del parametro in questione, se presente;
  • Il parametro di tipo jolly viene semplicemente rimosso;
  • In conseguenza della cancellazione, vengono inseriti degli opportuni cast, per ripristinare la coerenza tra tipi.

Di conseguenza, nel bytecode risultato della compilazione non c’è più traccia dei parametri di tipo.
Ovvero, in fase di esecuzione (run-time) i tipi parametrici sono scomparsi; fanno eccezione alcune funzionalità di riflessione (argomento di una lezione successiva), che sono in grado di recuperare anche a run-time alcuni parametri di tipo specificati nel sorgente.

Approcci alternativi

La cancellazione non è l’unico modo possibile di implementare la programmazione generica.

Ad esempio, il linguaggio C++ ha un meccanismo simile alla programmazione generica, chiamato template.
Le classi e i metodi template si presentano in modo molto simile alle classi e ai metodi parametrici in Java.
Quando il compilatore C++ trova un riferimento ad una certa versione di un template, come List< Integer >, istanzia una nuova copia della classe List, con il parametro di tipo sostituito da Integer.

In pratica, questo approccio è diametralmente opposto a quello di Java.
Come avremo modo di vedere, ciascuno ha i suoi pro e i suoi contro.

Istanziazione

Non è possibile utilizzare un parametro di tipo per istanziare oggetti, come in:
new T()         (errore di compilazione)

Infatti, al tempo di esecuzione tale istruzione diventerebbe equivalente a new Object(), che sicuramente non è quello che il programmatore intendeva ottenere.
D’altronde, è possibile utilizzare un parametro di tipo per istanziare una classe concreta, come in:
new LinkedList<T>()

Il parametro di tipo jolly, invece, non può essere utilizzato neanche per istanziare una classe concreta

new LinkedList<?>() (errore di compilazione)

Quest’ultima istruzione corrisponderebbe alla richiesta di creare una lista di oggetti di tipo sconosciuto.
Ricordiamo che il parametro di tipo jolly serve per avere riferimenti in grado di puntare a diverse versioni di una classe parametrica.

Istanziazione di array

Non è possibile istanziare un array di tipo parametrico

new T[10] (errore di compilazione)

Gli array ricordano il tipo con il quale sono stati creati, quindi questo array sarebbe a tutti gli effetti di tipo Object.
Una possibile soluzione consiste nell’istanziare una lista di tipo T, invece di un array.

Invece, è possibile dichiarare un riferimento di tipo array di tipo parametrico

T[] a;

Questo costrutto è utile, ad esempio, come parametro formale di un metodo, per accettare array di qualsiasi tipo ed associare il tipo dell’array a quello di altri parametri, oppure al tipo di ritorno.

Overloading

I parametri di tipo presentano una serie di problemi legati all’overloading.
Per prima cosa, non è possibile usare un parametro di tipo per distinguere due versioni di un metodo.

Consideriamo la classe Pair,<S,T> che rappresenta una coppia di elementi di due tipi diversi.
Si potrebbe essere tentati di realizzarla secondo il seguente schema:

public class Pair <S,T>{

private S first;
private T second;
public void setFirst(S x) { first = x; }
public void setSecond(T x) { second = x; }
(errore di compilazione)
...

}

Questa soluzione non funziona, perché entrambi i parametri di tipo, dopo il type checking, diventeranno Object, e quindi l’overloading diventerà non valido.

Overloading (segue)

Non è possibile utilizzare un parametro di tipo per selezionare una determinata versione di un metodo in overloading.

Ad esempio, consideriamo i seguenti metodi:

public void f(String s) { ... }
public void f(Object o) { ... }

public <T> void g(T x) { f(x); }

Indipendentemente dal valore assunto dal parametro di tipo T, il metodo g chiamerà sempre f(Object);
in particolare, anche se g sarà chiamato con T=String.

Infatti, la risoluzione dell’overloading avviene a tempo di compilazione, quando ancora non si conosce il valore che T assumerà;
il compilatore non può che assumere T=Object.

Riflessione e conversioni

A causa della cancellazione, i parametri di tipo non vanno usati per effettuare conversioni esplicite (cast) o controlli dinamici di tipo (instanceof).
In particolare, abbiamo:

(T) x;   (warning)

a run-time diventa un cast verso Object (o verso il primo limite superiore di T);

x instanceof T (errore di compilazione)

a run-time diventerebbe “x instanceof Object”.

Esempio
Nella lezione precedente, abbiamo esaminato il seguente metodo della classe java.util.Collections:

public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> l)

A questo punto, è possibile analizzare in dettaglio questa firma.
Per individuare l’elemento minimo di una collezione, il metodo min ha bisogno che gli elementi della collezione siano mutuamente confrontabili tramite l’interfaccia Comparable. Quindi, il parametro di tipo T, che rappresenta il tipo degli elementi della collezione, ha come limite superiore Comparable< ?super T >.

Inoltre, prima che esistessero i tipi parametrici in Java, questo metodo era già presente, con la firma

public static Object min(Collection l)

Il limite superiore < T extends Object & … > fa si che, dopo la cancellazione, la nuova firma coincida con la vecchia, a vantaggio della compatibilità con il codice pre-esistente.

Infine, il tipo del parametro Collection < ? extends T > offre la garanzia che la collezione passata al metodo min non sarà modificata.

Le annotazioni

Le annotazioni sono un meccanismo, introdotto in Java 1.5, che permette di associare meta-dati a parti del codice sorgente come classi, campi e metodi.
Le annotazioni si presentano come etichette precedute dal simbolo “@” (chiocciola), come in

@Override

L’annotazione precede l’elemento a cui si riferisce.
Esistono alcune annotazioni predefinite ed è anche possibile crearne di nuove.

Qui, esamineremo soltanto due delle annotazioni predefinite: @Override e @Deprecated.

L’annotazione @Override

L’annotazione “@Override” si applica ad un metodo per indicare che il programmatore ritiene che tale metodo effettui l’overriding di un metodo omonimo contenuto in una superclasse o super-interfaccia.
Il compilatore emette un warning se questa condizione non è verificata.
È utile applicare l’annotazione @Override a tutti i metodi che rappresentano un caso di overriding.
In tal modo, il compilatore ci avviserà se, ad esempio, abbiamo sbagliato qualcosa nella firma (il nome del metodo o il tipo di un parametro), magari rendendo il metodo definito un caso di overloading invece che di overriding.

Esempio:
public class Test {

@Override
public boolean equals() { ... }

@Override
public int hashcode() { ... }

}

Nella classe Test, l’annotazione riferita al metodo hashcode provoca un warning in compilazione, in quanto il nome del corrispondente metodo della classe Object è “hashCode”, con la “C” maiuscola.
Pertanto, il metodo hashcode della classe Test non è un caso di overriding.

L’annotazione @Deprecated

L’annotazione “@Deprecated” si applica ad un metodo per indicare che esso viene fornito per compatibilità con le versioni precedenti della classe in questione, ma che esso non dovrebbe essere utilizzato.
Il compilatore emette un warning per ogni chiamata a tale metodo.

Ad esempio, la classe Thread, che rappresenta un thread di esecuzione, prevede un metodo stop che forza la terminazione di un thread.
Dalla versione 1.5 di Java, tale metodo è stato dichiarato deprecato, utilizzando la seguente sintassi

public class Thread {

@Deprecated
public final void stop() { ... }

...

}

Quindi, ogni chiamata al metodo stop provocherà un warning in compilazione.
Il metodo è stato dichiarato deprecato in quanto ci si è resi conto che l’interruzione forzata di un thread può dare luogo a svariati problemi di sincronizzazione.
Per approfondimenti, cercare online l’articolo “Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated?”

  • Contenuti protetti da Creative Commons
  • Feed RSS
  • Condividi su FriendFeed
  • Condividi su Facebook
  • Segnala su Twitter
  • Condividi su LinkedIn
Progetto "Campus Virtuale" dell'Università degli Studi di Napoli Federico II, realizzato con il cofinanziamento dell'Unione europea. Asse V - Società dell'informazione - Obiettivo Operativo 5.1 e-Government ed e-Inclusion