Skip to content

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:

R call(T t)

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:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

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:

T: java.lang.Integer
U: java.lang.Long
T: java.lang.Integer
U: java.lang.Double

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:

1
2
3
4
5
java.lang.Double
java.lang.Long
java.lang.Integer
java.lang.Integer
java.lang.Integer

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.