3. Firme di classi e metodi generici
![]() |
La libreria RxJava dispone di molti metodi che accettano come parametri istanze di interfacce generiche. A volte, le loro firme sono complesse. 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 la definizione del parametro del metodo map, ad esempio: Func1<? super T,? extends R> func ?
Informazioni sui generici sono disponibili all'URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Alcune delle informazioni riportate di seguito provengono da questo URL. I generici sono stati introdotti nella versione 1.5 di Java.
Immaginiamo un servizio web che fornisce informazioni di vario tipo. Il servizio web potrebbe talvolta non riuscire a fornire queste informazioni e deve quindi segnalare un errore al proprio client. Possiamo quindi standardizzare la risposta del servizio web nella forma seguente:
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
...
}
- riga 1: la classe [Response] è parametrizzata dal tipo T;
- riga 9: il tipo T è il tipo del corpo della risposta, che è ciò che il client si aspetta effettivamente;
- riga 5: un codice di errore, 0 se non c'è alcun errore;
- riga 7: messaggi che spiegano l'errore, null se non c'è alcun errore;
Sul lato client, possiamo 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>] è utile in questo contesto perché ci permette di lavorare con una risposta standard.
Per creare un tipo [Response] utilizzando l'operatore new, scriviamo:
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 il tipo effettivo dei parametri generici sul lato destro del segno di uguale: verrà invece utilizzato il tipo formale specificato sul lato sinistro del segno di uguale. Questo processo è chiamato inferenza di tipo: il compilatore è in grado di determinare autonomamente il tipo effettivo dei parametri generici in base al contesto. Questa funzionalità è ampiamente utilizzata nelle funzioni lambda, dove il tipo effettivo dei parametri generici di un metodo viene spesso omesso.
Ora, sul lato client, il metodo [getDataFromWebService] potrebbe avere la seguente firma:
<T1,T2> Response<T1> getDataFromWebService(String urlWebService, String httpMethod, T2 post)
Qui abbiamo un metodo generico parametrizzato da due tipi, T1 e T2:
- T1 è il tipo del corpo della risposta prevista;
- 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 quando si interroga questo URL;
Sul lato client, potremmo avere la seguente chiamata:
Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;
In questo caso, avremo T1=Product e T2=Long.
Con Java 8, possiamo avvalerci dell'inferenza dei tipi e scrivere 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 può imporre vincoli ai propri parametri generici. Rivediamo la definizione del metodo [map] in 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 costituiscono la base delle espressioni 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] si aspetta un singolo 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 supertipo di T;
- ? extends R: il tipo del risultato restituito dal metodo [call] deve essere di tipo R o un sottotipo se R è una classe, oppure implementare R se R è un'interfaccia;
Per comprendere meglio i due vincoli sopra indicati, esaminiamo 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"); // error: this is still String!
}
}
- riga 3: la classe [Box] è parametrizzata dal tipo T, che è il tipo del campo alla riga 5;
- riga 15: il metodo [inspect] accetta un parametro di tipo U. Un metodo può anche avere parametri generici. In tal caso viene dichiarato come segue:
Qui, U è dichiarato come tipo generico del metodo utilizzando <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, segui questi passaggi [1, 2]:
![]() |
Ora, considera 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 una List<T>, dove T è una superclasse della classe [Integer]. A causa di questa restrizione, è possibile aggiungere alla lista i numeri interi da 1 a 3 (righe 2–4) e il metodo potrebbe essere chiamato come segue:
List<Number> numbers=new ArrayList<>();
// ajout double Number <-- Double
numbers.add(7.8);
// ajout Long Number <-- Long
numbers.add(1L);
// ajout List<Number> Number <-- Integer
addNumbers(numbers);
// affichagede List<Number>
for(Number number : numbers){
System.out.println(number.getClass().getName());
}
Questo produce il seguente risultato:
Per estendere la classe Box<T>, scriveremmo:
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);
}
}
Un'interfaccia può anche utilizzare parametri generici:
interface I<T> {
void doSomethingWith(T t);
}
Per implementarla, utilizzare 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.


