Skip to content

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:

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

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:

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

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:

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>, 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.