In questo periodo mi sto cimentando con la certificazione
della Oracle Java SE 6 Programmer (ex SCJP). In generale, mi sono accorto che
la conoscenza di Java approfondita aiuta tantissimo nella programmazione. Molti aspetti vengono
completamente ignorati e con il tempo le cose diventano meccaniche. Tuttavia,
conoscere una tecnologia ha un significato, a mio avviso, molto
più profondo. Padroneggiare Java al pieno delle sue funzionalità non è semplice
e bisogna entrare in alcuni ragionamenti per poterli comprendere al meglio e
non dimenticarli con il passare del tempo. In questo articolo vorrei parlare di
un caso di utilizzo di due concetti molto importanti: Generics e Wildcard. In
particolare, svolgendo alcuni esercizi mi sono imbattuto in alcuni utilizzi
delle Wildcard nell’ambito dei Generics che mi risultavano piuttosto complessi.
Grazie alla complicità del web sono riuscito a trovare la giusta chiave di
lettura che vi mostrerò più in avanti.
In questo articolo non fornirò una spiegazione esaustiva dei generics. Mi limiterò ad un piccolo e utile richiamo.
In questo articolo non fornirò una spiegazione esaustiva dei generics. Mi limiterò ad un piccolo e utile richiamo.
Generics:
Gli Arrays in Java sono sempre stati Type Safe. Ciò vuol
dire che un array dichiarato di tipo Stringa (String []) non può accettare un
Integer, un oggetto Persona o qualsiasi altro tipo di oggetto. Ciò non è mai
stato vero per le Collection prima della versione 5 di Java.
Per creare un ArrayList di stringhe bastava indicare:
ArrayList myList = new ArrayList();
oppure
List myList = new ArrayList();
Prima della versione 5, non c’era un modo per specificare
che il myList era caratterizzato da elementi di tipo String. In questo modo, il
compilatore non era in grado di verificare se gli elementi inseriti nella lista
erano del tipo specificato. Ciò aveva un impatto negativo sui possibili errori
a runtime. Dalla versione 5 di Java sono stati introdotti i Generics. I
vantaggi nell’utilizzo di questi sono:
- Controllo della tipizzazione a tempo di compilazione. In questo modo si sfruttano tutti i vantaggi nel correggere errori a tempo di compilazione piuttosto che a runtime.
- Eliminazione del CASTING
- Permettono agli sviluppatori di implementare algoritmi generici che lavorano con differenti tipi che possono essere personalizzati.
Wildcard:
Supponiamo di voler stampare a video tutti gli elementi di
una Collection qualsiasi. Nel caso di una versione Java Pre-5 scriveremmo
questo codice:
void stampaElementiCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k <
c.size(); k++) {
System.out.println(i.next());
}
}
Dalla versione 5 in poi utilizzeremmo i generics in questo
modo:
void stampaElementiCollection(Collection<Object> c)
{
for (Object e : c) {
System.out.println(e);
}
}
Dalla teoria di Java sappiamo bene che il metodo appena
scritto considera solo le Collection<Object> che non è un supertipo di
qualsiasi Collection. Infatti, se provassimo a passare al metodo una
Collection<Integer> otterremmo un errore. Ciò rende questa soluzione
“meno usabile” rispetto a quella Pre-Java 5.
Per questo motive è stato introdotto un carattere jolly
indicato con il “?”, o meglio wildcard.
Attraverso l’utilizzo di questo carattere è possibile
limitare, attraverso dei vincoli, i possibili tipi che possono essere assegnati
ad una Collection (bounded wildcard). Ciò viene effettuato attraverso gli
extends e i super. Facciamo un esempio
List<? extends Animale>
lista;
Il riferimento creato può contenere qualsiasi istanza di
List con un parametro che può essere o di tipo Animale oppure qualsiasi suo
sottotipo. Un altro tipo di utilizzo delle wildcard è con la parola chiave
super:
List <? super Cane
> lista;
Può contenere istanze di List che contengono oggetti di tipo
Cane o suoi supertipi.
Utilizzo di Generics
e Wildcard
A questo punto veniamo all’argomento principale
dell’articolo. Nella certificazione dell’Oracle capitano spesso domande di
questo tipo ed è bene capire il ragionamento del compilatore per evitare di
“imparare” a memoria la risposta esatta.
Supponiamo di avere una classe Nodo che utilizza i Generics
come mostrato:
public class Nodo<E> {
E
e;
public void setE(E e){
this.e=e;
}
public E getE(){
return e;
}
}
A questo punto creiamo una classe che utilizza la classe
Nodo.
class TestNodo{
public static void main(String... args){
Nodo<? extends Number>n=new
Nodo<Number>();
n.setE(……………….); // Line1
…………. o=n.getE(); // Line2
Nodo<? super Number>n1=new Nodo<Number>();
n1.setE(…………….); // Line3
…………. o1=n1.getE(); // Line4
}
}
- Cosa il compilatore permette di passare al metodo Set()? (Line1)
Ragioniamo sulle informazioni che ha a
disposizione il compilatore. Abbiamo
creato un nodo a cui è associato un elemento di un tipo che estende la
classe Number . Questo elemento potrebbe essere di tipo Integer, Double, etc.
Se provassimo a passare un Integer al metodo set potrebbe accadere che questo
venga sfruttato come Double provocando così degli errori a runtime. Il
compilatore è molto premuroso e cerca di evitare tutto ciò che potrebbe essere
pericoloso per il nostro software. Per questo motivo, qualsiasi elemento
proviamo ad inserire otteniamo un errore dal compilatore del tipo:
The method setE(capture#1-of ? extends Number) in the
type Nodo<capture#1-of ? extends Number> is not applicable for the
arguments (Integer)
Ebbene, l’unico valore che il
compilatore di lascerà passare sarà il null. L’unica operazione valutata
“sicura”.
- Cosa ritorna il metodo get? (Line2)
Anche qui è fondamentale capire
come il compilatore ragiona. L’unica informazione che possiede è che nel nodo
l’elemento associato è di tipo Number o suoi sottotipi. Per questo motivo il
valore dedotto sarà necessariamente Number. In questo modo qualsiasi elemento
verrebbe referenziato in maniera corretta senza operare nessun tipo di cast.
Per questo il tipo di ritorno potrà essere un Number o suoi supertipi come per
esempio Object.
- Cosa il compilatore permette di passare al metodo Set()? (Line3)
Il compilatore sa che al Nodo è
associato un elemento che è supertipo di Number. Se, per esempio, passassimo al metodo set un
Integer, all’interno del metodo potrebbe esserci una istruzione del tipo Number
var = “qualcosa che è Integer” (il
Number è il risultato della traduzione del Generics). Questa operazione è
perfettamente legale. Se invece si passasse un Object si otterrebbe qualcosa
del tipo Number var = “qualcosa che è
Object”. Ciò è una operazione non valida. Per questo motivo il compilatore
ci lascerà passare qualsiasi oggetto di tipo Number o suoi sottotipi.
- Cosa ritorna il metodo get? (Line4)
Qui il compilatore sa che il tipo utilizzato
è un supertipo di Number. Per questo motivo il tipo “più sicuro” da tornare è
solo il tipo Object.
0 commenti:
Posta un commento