Generics e Tipi Limitati in Java
- I tipi generici possono essere limitati per accettare solo determinati tipi di classi.
- I tipi limitati sono definiti utilizzando la clausola
extends
durante la dichiarazione del parametro di tipo. - Si possono specificare più interfacce come limiti, utilizzando l'operatore
&
per creare un tipo intersezione. - I tipi limitati aiutano a garantire che solo tipi specifici vengano utilizzati con i generici, migliorando la sicurezza del tipo e prevenendo errori di runtime.
Tipi Limitati
Negli esempi delle lezioni precedenti sui tipi generici, i parametri di tipo potevano essere sostituiti da qualsiasi tipo di classe.
Questo va bene per molti scopi, ma a volte è utile limitare i tipi che possono essere passati a un parametro di tipo.
Ad esempio, supponiamo che si voglia creare una classe generica che contenga un metodo che restituisce la media di un array di numeri. Inoltre, si vuole usare la classe per ottenere la media di un array di qualsiasi tipo di numero, inclusi interi, float
e double
. Quindi, si vuole specificare il tipo dei numeri genericamente, usando un parametro di tipo. Per creare una tale classe, potreste provare qualcosa come questo:
// Questo codice è un tentativo (sbagliato)
// di creare una classe generica che calcola
// la media di un array di numeri di
// qualsiasi tipo dato.
// La classe contiene un errore!
class Statistiche<T> {
// numeri è un array di tipo T
T[] numeri;
// Passa al costruttore un riferimento a
// un array di tipo T.
Statistiche(T[] o) {
numeri = o;
}
// Restituisce tipo double in tutti i casi.
double media() {
double somma = 0.0;
for(int i=0; i < numeri.length; i++)
// Errore!!!
somma += numeri[i].doubleValue();
return somma / numeri.length;
}
}
In Statistiche
, il metodo media()
tenta di ottenere la versione double
di ogni numero nell'array numeri
chiamando doubleValue()
.
Poiché tutte le classi numeriche, come Integer
e Double
, sono sottoclassi di Number
, e Number
definisce il metodo doubleValue()
, questo metodo è disponibile a tutte le classi wrapper numeriche.
Il problema è che il compilatore non ha modo di sapere che si intende creare oggetti Statistiche
usando solo tipi numerici. Quindi, quando si prova a compilare Statistiche
, viene riportato un errore che indica che il metodo doubleValue()
è sconosciuto. Per risolvere questo problema, bisogna avere a disposizione di qualche modo per dire al compilatore che si vuole passare solo tipi numerici a T
. Inoltre, c'è bisogno di qualche modo per assicurare che solo tipi numerici vengano effettivamente passati.
Per gestire tali situazioni, Java fornisce i tipi limitati.
Quando si specifica un parametro di tipo, si può creare un limite superiore che dichiara la superclasse da cui tutti gli argomenti di tipo devono essere derivati. Questo è realizzato attraverso l'uso di una clausola extends
quando si specifica il parametro di tipo, come mostrato qui:
<T extends superclasse>
Questa riga specifica che T
può essere sostituito solo da superclasse
, o sottoclassi di superclasse
. Quindi, superclasse
definisce un limite superiore inclusivo.
Si può usare un limite superiore per correggere la classe Statistiche
mostrata prima specificando Number
come limite superiore, come mostrato qui:
// In questa versione di Statistiche, l'argomento di tipo per
// T deve essere o Number, o una classe derivata
// da Number.
class Statistiche<T extends Number> {
// numeri è un array di tipo T
// ma T è limitato a Number o sottoclasse.
T[] numeri;
// Passa al costruttore un riferimento a
// un array di tipo Number o sottoclasse.
Statistiche(T[] o) {
numeri = o;
}
// Restituisce tipo double in tutti i casi.
double media() {
double somma = 0.0;
for(int i=0; i < numeri.length; i++)
somma += numeri[i].doubleValue();
return somma / numeri.length;
}
}
// Dimostra Statistiche.
class DemoLimiti {
public static void main(String[] args) {
// Crea un array di Integer e calcola la media.
Integer[] interi = { 1, 2, 3, 4, 5 };
Statistiche<Integer> oggettoIntero =
new Statistiche<Integer>(interi);
double v = oggettoIntero.media();
System.out.println("oggettoIntero media è " + v);
// Crea un array di Double e calcola la media.
Double[] decimali = { 1.1, 2.2, 3.3, 4.4, 5.5 };
Statistiche<Double> oggettoDouble =
new Statistiche<Double>(decimali);
double w = oggettoDouble.media();
System.out.println("oggettoDouble media è " + w);
// Questo non compilerà perché String non è una
// sottoclasse di Number.
// String[] stringhe = { "1", "2", "3", "4", "5" };
// Statistiche<String> oggettoStringa = new Statistiche<String>(stringhe);
// double x = oggettoStringa.media();
// System.out.println("oggettoStringa media è " + v);
}
}
L'output è mostrato qui:
oggettoIntero media è 3.0
oggettoDouble media è 3.3
Si noti come Statistiche
è ora dichiarata da questa riga:
class Statistiche<T extends Number> {
/* ... */
}
Poiché il tipo T
è ora limitato da Number
, il compilatore Java sa che tutti gli oggetti di tipo T
possono chiamare doubleValue()
perché è un metodo dichiarato da Number
.
Questo è, di per sé, un grande vantaggio. Tuttavia, come bonus aggiuntivo, la limitazione di T
previene anche che oggetti Statistiche
non numerici vengano creati. Ad esempio, se si prova a rimuovere i commenti dalle righe alla fine del programma, e poi si prova a ricompilare, si riceveranno errori di compilazione perché String
non è una sottoclasse di Number
.
Oltre a usare un tipo di classe come limite, si può anche usare un tipo di interfaccia. Infatti, si può specificare più interfacce come limiti. Inoltre, un limite può includere sia un tipo di classe che una o più interfacce. In questo caso, il tipo di classe deve essere specificato per primo. Quando un limite include un tipo di interfaccia, solo argomenti di tipo che implementano quell'interfaccia sono legali. Quando si specifica un limite che ha una classe e un'interfaccia, o più interfacce, bisogna usare l'operatore &
per connetterle. Questo crea un tipo intersezione. Ad esempio,
class Gen<T extends MiaClasse & MiaInterfaccia> {
/* ... */
}
Qui, T
è limitato da una classe chiamata MiaClasse
e un'interfaccia chiamata MiaInterfaccia
. Quindi, qualsiasi argomento di tipo passato a T
deve essere una sottoclasse di MiaClasse
e implementare MiaInterfaccia
.