Quando si dichiara un parametro di tipo, appartenente ad una classe oppure ad un metodo, si può specificare che quel parametro potrà assumere come valore solo sottotipi di un tipo dato.
Questo tipo viene chiamato limite superiore per il parametro in questione.
Un parametro di tipo può anche avere più limiti superiori simultaneamente
La sintassi per indicare un o più limiti superiori è la seguente:
<T extends U1 & U2 & ... >
U1, U2, etc. sono tutte interfacce, oppure
U1 è una classe e i successivi (eventuali) tipi sono tutti interfacce.
In altre parole, solo uno dei limiti superiori può essere una classe
Le due interfacce Comparable e Comparator, già oggetto di una lezione precedente, sono in realtà parametriche:
public interface Comparable<T> {
public int compareTo(T x);
}
public interface Comparator
<T>
{
public int compare(T x, T y);
}
Come si vede, il parametro di tipo permette di specificare che tipo di oggetti tali metodi sono in grado di confrontare.
Ad esempio, se la classe Employee intende fornire un ordinamento naturale tra impiegati (ad esempio, ordinamento alfabetico per nome), essa si presenterà come segue:
public class Employee implements Comparable
<Employee>
.
Supponiamo di voler scrivere un metodo statico che accetta una lista di oggetti tra loro confrontabili, e restituisce l’elemento minimo della lista.
I limiti superiori permettono di accettare, come parametro di tipo, soltanto un tipo che implementi Comparable.
Siccome Comparable è a sua volta parametrica, bisogna specificare anche il suo parametro di tipo.
Otteniamo quindi la seguente soluzione:
Se Employee
implementa Comparable<Employee>
, è possibile passare al metodo precedente una lista di Employee
.
Supponiamo poi che la classe Manager
estenda Employee
, e non implementi direttamente nessuna versione di Comparable
.
Indirettamente, Manager
implementerà anch’essa Comparable<Employee>
.
In questa situazione, non è possibile passare al metodo getMin
una lista di Manager
, perché la classe Manager
non implementa Comparable<Manager>
.
Le prossime slide mostrano come ovviare a questa limitazione.
Una delle regole di sottotipo afferma che se A è sottotipo di B, array di A è sottotipo di array di B.
Questo principio non vale per i tipi parametrici.
Ovvero, se X è una classe con un parametro di tipo, ed A e B sono due tipi tali che A è sottotipo di B, non è vero che X<A>
è sottotipo di X<B>
.A questo proposito, è interessante confrontare il comportamento di array e collezioni, rispetto ai sottotipi.
Nel caso degli array, consideriamo il seguente frammento di codice:
Manager[] man_arr = new Manager[10];
Employee[] emp_arr = man_arr;
emp_arr[0] = new Employee(...);
Questo frammento compila correttamente, grazie alle regole di sottotipo per array.
Tuttavia, il terzo rigo provoca un’eccezione al run-time.
Infatti, non è possibile aggiungere un Employee
ad un array che è stato dichiarato.
La prossima slide illustra cosa succede in una situaziona analoga, che utilizza ArrayList
invece di semplici array.
Possiamo tentare di sviluppare un esempio simile a quello della slide precedente, sostituendo gli array con oggetti di tipo ArrayList
ArrayList<Manager> man_list = new ArrayList<Manager>();
ArrayList<Employee> emp_list = man_list;
emp_list.add(new Employee(...));
Questa volta, il frammento di codice provoca un errore di compilazione.
In particolare, l’errore si verifica al secondo rigo, perché ArrayList<Manager>
non è sottotipo di (e quindi non è assegnabile a) ArrayList<Employee>
.
La situazione è quindi migliore della precedente, perché quello che prima era un errore al run-time ora è diventato un errore di compilazione, molto più facile da individuare, e quindi correggere.
Concludiamo che il sistema dei tipi parametrici è più robusto di quello degli array, rispetto agli errori di tipo.
Infatti, le regole relative ai tipi parametrici garantiscono che, se il programma viene compilato senza warning e se non utilizza array o cast, non possono verificarsi errori di tipo al run-time.
Le regole appena viste per la relazione di sottotipo tra tipi parametrici comportano, tra l’altro, che List<Object>
non sia il supertipo comune a tutte le liste.
Quindi, sembrerebbe che, se un metodo intende accettare liste di ogni tipo, esso debba necessariamente essere parametrico.
Invece, il parametro di tipo jolly, rappresentato sintatticamente da un punto interrogativo, permette di raggiungere lo stesso scopo senza utilizzare parametri di tipo.
Ovvero, List<?>
è il supertipo comune a tutte le versioni di List.
Il parametro di tipo jolly si può usare solamente come parametro attuale di tipo, ed intuitivamente rappresenta un tipo sconosciuto.
Non c’è nessuna relazione tra due diverse occorrenze del parametro jolly.
Ad esempio, il seguente metodo accetta due liste dello stesso tipo
public static <T> void addAll(List<T> l1, List<T> l2) { ... }
mentre il seguente metodo accetta due liste qualsiasi
public static void addAll(List<?> l1, List<?> l2) { ... }
Supponiamo che A<T>
sia una classe parametrica.
Dichiarando un riferimento di tipo A<?>
, è come se le occorrenze di T all’interno di A diventassero “?”, cioè, “tipo sconosciuto”.
Questo ha determinate conseguenze sulle chiamate a metodi fatte tramite tale riferimento.
In particolare, se un metodo della classe A<T>
accetta un argomento di tipo T, usando un riferimento di tipo A<?>
potremo passare a quel metodo solamente null
.
Infatti, non sapendo qual è il tipo concreto che quel metodo si aspetta, null
è l’unico valore che è lecito passargli.
Se invece un metodo della classe A<T>
restituisce valori di tipo T, chiamandolo con un riferimento di tipo A<?>
potremo assegnare il valore restituito solamente ad un riferimento di tipo Object
.
Infatti, Object
è l’unico tipo che sicuramente accetta qualunque tipo sconosciuto (esclusi i tipi base).
Come i normali parametri di tipo, anche il parametro di tipo jolly può essere dotato di un limite superiore, ma non più di uno.
Ad esempio, List<? extends Employee>
è una lista di tipo sconosciuto che estende Employee
.
Precisamente, List<? extends Employee>
è il supertipo comune a tutte le liste il cui parametro di tipo estende Employee
.
A differenza dei normali parametri di tipo, il tipo jolly può anche avere un limite inferiore.
Ad esempio, List<? super Employee>
rappresenta una lista di un tipo sconosciuto che è supertipo di Employee
(come una ipotetica classe Person
, oppure Object
).
Consideriamo nuovamente il problema di scrivere un metodo che accetta una lista di oggetti tra loro confrontabili, e restituisce l’elemento minimo della lista.
Il parametro di tipo jolly permette di dichiarare il metodo in modo tale da accettare una lista di Manager
, anche se Manager
implementa Comparable<Employee>
:
public static <T extends Comparable<? super T>> T getMin(List<T> l) { ... }
In effetti, un metodo molto simile si trova nella classe java.util.Collections
della libreria standard:
public static <T extends Object & Comparable<? super T>> T min(Collection<? extends T> l) { ... }
I dettagli di questa firma verranno esaminati nella lezione successiva.
Come abbiamo visto per il tipo jolly senza limiti, l’uso del parametro jolly con limiti superiori o inferiori impone determinate condizioni sulle chiamate ai metodi.
Supponiamo che A<T>
sia una classe parametrica.
La seguente tabella riassume le limitazioni che valgono per un riferimento di tipo A<?>
, A<? extends B>
, oppure A<? super B>
, rispetto ad un metodo di A che accetta un argomento di tipo T, oppure restituisce un valore di tipo T.
4. Risoluzione dell'overloading e dell'overriding
5. Controllo di uguaglianza tra oggetti
6. Classi interne, locali ed anonime
7. Iteratori, teoria e pratica
8. Clonazione di oggetti. Confronto tra oggetti.
9. Elementi di programmazione di interfacce grafiche
10. Il paradigma Model-View-Controller. Il pattern Strategy
11. I pattern Composite e Decorator
12. I pattern Template Method e Factory Method
13. Classi e metodi parametrici
14. La libreria Java Collection Framework: le interfacce Iterable, ...
15. La libreria Java Collection Framework: la classe HashSet e le l...
16. Parametri di tipo con limiti
17. L'implementazione della programmazione generica: la cancellazio...
18. La riflessione
19. Introduzione al multi-threading
22. Classi enumerate