3. Le firme delle classi e dei metodi generici
![]() |
La libreria RxJava dispone di numerosi metodi che accettano come parametri istanze di interfacce generiche. A volte, la firma di queste ultime è complessa. Ecco alcuni esempi:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
public final <R> Observable<R> flatMap(Func1<? super T,? extends Observable<? extends R>> func)
Questi due metodi utilizzano due tipi generici T e R. Ma cosa significa, ad esempio, la definizione del parametro del metodo map: Func1<? super T,? extends R> func ?
Le informazioni sui generici sono disponibili su URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Alcune delle informazioni riportate di seguito provengono da URL. I generici sono stati introdotti con la versione 1.5 di Java.
Immaginiamo un servizio web che fornisce informazioni di vario tipo. Il servizio web può talvolta non riuscire a fornire tali informazioni e deve quindi segnalare un errore al proprio cliente. È quindi possibile standardizzare la risposta del servizio web nella forma seguente:
public class Response<T> {
// ----------------- proprietà
// stato dell'operazione
private int status;
// eventuali messaggi di errore
private List<String> messages;
// il corpo della risposta
private T body;
...
}
- riga 1: la classe [Response] è parametrizzata dal tipo T;
- riga 9: il tipo T è il tipo del corpo della risposta, ovvero ciò che il cliente si aspetta effettivamente;
- riga 5: un codice di errore, 0 se non c'è errore;
- riga 7: messaggi che spiegano l’errore, null se non c’è errore;
Dal lato client, si potrà quindi scrivere:
Response<Product> product=getDataFromWebService(...) ;
oppure
Response<List<Product>>=getDataFromWebService(...) ;
Il tipo formale T viene sostituito da un tipo effettivo, in questo caso [Product] o [List<Product>]. La classe generica [Response<T>] risulta qui utile in quanto consente di lavorare con una risposta standard.
Per creare un tipo [Response] con l'operatore new, si scriverà:
Response<List<Product>> response=new Response<List<Product>> (...) ;
A partire dalla versione 1.8 di Java, l’istruzione precedente può essere scritta in modo più semplice:
Response<List<Product>> response=new Response<> (...) ;
Non è più necessario specificare, a destra del segno =, il tipo effettivo dei parametri generici: verrà utilizzato il tipo formale specificato a sinistra del segno =. Questo processo è noto come inferenza di tipo: il compilatore è in grado di individuare autonomamente il tipo effettivo dei parametri generici in base al contesto. Questa caratteristica è ampiamente utilizzata nelle funzioni lambda, dove il tipo effettivo dei parametri generici di un metodo viene spesso omesso.
Ora, dal lato client, il metodo [getDataFromWebService] potrebbe avere la seguente firma:
<T1,T2> Response<T1> getDataFromWebService(String urlWebService, String httpMethod, T2 post)
Si tratta di un metodo generico parametrizzato da due tipi, T1 e T2:
- T1 è il tipo del corpo della risposta attesa;
- T2 è il tipo del valore inviato per un'operazione POST, null per un'operazione GET;
- urlWebService: è l'URL del servizio web;
- httpMethod: è il metodo HTTP, GET o POST da utilizzare per interrogare questo URL;
Dal lato client, si potrà avere la seguente chiamata:
Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
In questo caso si avrà T1=Product e T2=Long.
Con Java 8 è possibile utilizzare l'inferenza di tipo e si scriverà in modo più semplice:
Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
Ecco un’altra possibile chiamata:
Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;
In questo caso avremo T1=List<Product> e T2=Long[].
La classe o il metodo generico possono imporre vincoli sui propri parametri generici. Torniamo alla definizione del metodo [map] di RxJava:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
Il metodo accetta due tipi di parametri, T e R. Il tipo [Func1] è un’interfaccia generica:
![]() |
Func1 definisce un'interfaccia funzionale, ovvero un'interfaccia con un unico metodo. Le interfacce funzionali sono alla base delle funzioni lambda di Java 8. In questo caso, il metodo dell'interfaccia è definito come:
Quindi T è il tipo del parametro passato a [call] e R il tipo del risultato ottenuto. Torniamo alla definizione del metodo [map]:
public final <R> Observable<R> map(Func1<? super T,? extends R> func)
- il metodo [map] accetta un unico parametro di tipo [Func1<? super T,? extends R>] e restituisce un tipo [Observable<R>];
- ? super T: il parametro passato al metodo [Func1.call] deve essere di tipo T o un tipo derivato da T;
- ? extends R: il tipo del risultato restituito dal metodo [call] deve essere di tipo R o derivato da R se R è una classe, oppure deve implementare R se R è un'interfaccia;
Per comprendere meglio i due vincoli precedenti, prendiamo alcuni esempi tratti dal riferimento [https://docs.oracle.com/javase/tutorial/java/generics/bounded.html]:
package tests;
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u) {
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<>();
integerBox.set(new Integer(10));
integerBox.inspect(new Long(20));
integerBox.inspect(new Double(-1.78));
//integerBox.inspect("some text"); // errore: questo è ancora uno String!
}
}
- riga 3: la classe [Box] è parametrizzata dal tipo T, che è il tipo del campo della riga 5;
- riga 15: il metodo [inspect] accetta un parametro di tipo U. Anche un metodo può avere parametri generici. In tal caso, viene dichiarato come segue:
Qui, U è dichiarato come tipo generico del metodo tramite <U>, ma con un vincolo <U extends Number>, ovvero U deve estendere la classe [Number]. A causa di questo vincolo, il compilatore segnala un errore alla riga 25;
- righe 23-24: chiamate al metodo [inspect] con tipi derivati da [Number];
Si ottengono i seguenti risultati:
Nota: per eseguire la classe in IntelliJ, procedere come segue [1, 2]:
![]() |
Ora consideriamo il seguente esempio:
public void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 3; i++) {
list.add(i);
}
}
Il metodo [addNumbers] accetta come parametro un tipo List<T>, dove T è una classe padre della classe [Integer]. A causa di questa restrizione, i numeri interi da 1 a 3 (int) possono essere aggiunti alla lista (righe 2-4) e il metodo potrebbe essere chiamato nel modo seguente:
List<Number> numbers=new ArrayList<>();
// aggiunta di un numero di tipo Double <-- Double
numbers.add(7.8);
// aggiunta di un numero di tipo Long <-- Long
numbers.add(1L);
// aggiunta di un numero a una Lista<Numero> <-- Integer
addNumbers(numbers);
// visualizzazione di List<Number>
for(Number number : numbers){
System.out.println(number.getClass().getName());
}
Si ottiene quindi il seguente risultato:
Per estendere la classe Box<T>, si scriverà:
class OtherBox<T> extends Box<T> {
}
La classe figlia può a sua volta introdurre nuovi parametri generici:
class AnotherBox<T, T1> extends Box<T> {
void consumer(T1 t1) {
System.out.printf("%s%n", t1);
}
}
Anche un'interfaccia può utilizzare parametri generici:
interface I<T> {
void doSomethingWith(T t);
}
Per implementarla, si utilizza la seguente sintassi:
class A<T> implements I<T> {
@Override
public void doSomethingWith(T t) {
System.out.printf("%s%n", t);
}
}
Ora sappiamo abbastanza sui generici per affrontare le funzioni lambda.


