Skip to content

3. Firmas de clases y métodos genéricos

  

La biblioteca RxJava cuenta con muchos métodos que aceptan instancias de interfaces genéricas como parámetros. A veces, sus firmas son complejas. A continuación se muestran algunos ejemplos:


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)

Estos dos métodos utilizan dos tipos genéricos, T y R. Pero, ¿qué significa la definición del parámetro del método map, por ejemplo: Func1<? super T,? extends R> func ?

Se puede encontrar información sobre genéricos en la URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Parte de la información que se ofrece a continuación procede de esta URL. Los genéricos se introdujeron en la versión 1.5 de Java.

Imaginemos un servicio web que proporciona información de diversos tipos. En ocasiones, el servicio web puede no poder proporcionar esta información y, en ese caso, debe notificar un error a su cliente. Podemos entonces estandarizar la respuesta del servicio web de la siguiente forma:


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;
...
}
  • línea 1: la clase [Response] está parametrizada por el tipo T;
  • línea 9: el tipo T es el tipo del cuerpo de la respuesta, que es lo que el cliente realmente espera;
  • línea 5: un código de error, 0 si no hay error;
  • línea 7: mensajes que explican el error, null si no hay error;

En el lado del cliente, podemos escribir entonces:


Response<Product> product=getDataFromWebService(...) ;

o


Response<List<Product>>=getDataFromWebService(...) ;

El tipo formal T se sustituye por un tipo real, en este caso [Product] o [List<Product>]. La clase genérica [Response<T>] resulta útil aquí porque nos permite trabajar con una respuesta estándar.

Para crear un tipo [Response] utilizando el operador new, escribimos:


Response<List<Product>> response=new Response<List<Product>> (...) ;

Desde la versión 1.8 de Java, la instrucción anterior se puede escribir de forma más sencilla:


Response<List<Product>> response=new Response<> (...) ;

Ya no es necesario especificar el tipo real de los parámetros genéricos a la derecha del signo igual: en su lugar, se utilizará el tipo formal especificado a la izquierda del signo igual. Esto se denomina inferencia de tipos: el compilador es capaz de determinar por sí mismo el tipo real de los parámetros genéricos basándose en el contexto. Esta característica se utiliza ampliamente en las funciones lambda, donde a menudo se omite el tipo real de los parámetros genéricos de un método.

Ahora, en el lado del cliente, el método [getDataFromWebService] podría tener la siguiente firma:


<T1,T2> Response<T1>  getDataFromWebService(String urlWebService, String httpMethod, T2 post)

Aquí tenemos un método genérico parametrizado por dos tipos, T1 y T2:

  • T1 es el tipo del cuerpo de la respuesta esperada;
  • T2 es el tipo del valor enviado para una operación POST, nulo para una operación GET;
  • urlWebService: es la URL del servicio web;
  • httpMethod: es el método HTTP GET o POST que se utilizará al consultar esta URL;

En el lado del cliente, podríamos tener la siguiente llamada:


Long id=... ;
Response<Product> product=this.<Product,Long>getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

En este caso, tendremos T1=Product y T2=Long.

Con Java 8, podemos utilizar la inferencia de tipos y escribir el código de forma más sencilla:


Long id=... ;
Response<Product> product=getDataFromWebService('http://localhost:8080/rest/product','POST',id) ;

Aquí hay otra llamada posible:


Long[] ids=... ;
Response<List<Product>> product=getDataFromWebService('http://localhost:8080/rest/products','POST',ids) ;

En este caso, tendremos T1=List<Product> y T2=Long[].

La clase o método genérico puede imponer restricciones a sus parámetros genéricos. Repasemos la definición del método [map] en RxJava:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)

El método toma dos tipos de parámetros, T y R. El tipo [Func1] es una interfaz genérica:

 

Func1 define una interfaz funcional, es decir, una interfaz con un único método. Las interfaces funcionales constituyen la base de las expresiones lambda de Java 8. En este caso, el método de la interfaz se define como:

R call(T t)

Así pues, T es el tipo del parámetro pasado a [call] y R es el tipo del resultado obtenido. Volvamos a la definición del método [map]:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)
  • el método [map] espera un único parámetro de tipo [Func1<? super T,? extends R>] y devuelve un tipo [Observable<R>];
  • ? super T: el parámetro pasado al método [Func1.call] debe ser de tipo T o un supertipo de T;
  • ? extends R: el tipo del resultado devuelto por el método [call] debe ser de tipo R o un subtipo si R es una clase, o implementar R si R es una interfaz;

Para comprender mejor las dos restricciones anteriores, veamos algunos ejemplos de la referencia [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!
    }
}
  • línea 3: la clase [Box] está parametrizada por el tipo T, que es el tipo del campo de la línea 5;
  • línea 15: el método [inspect] toma un parámetro de tipo U. Un método también puede tener parámetros genéricos. En ese caso, se declara de la siguiente manera:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

Aquí, U se declara como el tipo genérico del método utilizando <U>, pero con una restricción <U extends Number>, es decir, U debe extender la clase [Number]. Debido a esta restricción, el compilador informa de un error en la línea 25;

  • líneas 23-24: llamadas al método [inspect] con tipos derivados de [Number];

Se obtienen los siguientes resultados:

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

Nota: Para ejecutar la clase en IntelliJ, sigue estos pasos [1, 2]:

 

Ahora, considere el siguiente ejemplo:


    public void addNumbers(List<? super Integer> list) {
        for (int i = 1; i <= 3; i++) {
            list.add(i);
        }
}

El método [addNumbers] toma una Lista<T> como parámetro, donde T es una superclase de la clase [Integer]. Debido a esta restricción, se pueden añadir a la lista los números enteros del 1 al 3 (líneas 2-4), y el método se podría invocar de la siguiente manera:


    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());
}

Esto produce el siguiente resultado:

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

Para extender la clase Box<T>, escribiríamos:


class OtherBox<T> extends Box<T> {
 
}

La clase derivada puede introducir a su vez nuevos parámetros genéricos:


class AnotherBox<T, T1> extends Box<T> {
    void consumer(T1 t1) {
        System.out.printf("%s%n", t1);
    }
}

Una interfaz también puede utilizar parámetros genéricos:


interface I<T> {
    void doSomethingWith(T t);
}

Para implementarla, utiliza la siguiente sintaxis:


class A<T> implements I<T> {
 
    @Override
    public void doSomethingWith(T t) {
        System.out.printf("%s%n", t);
    }
 
}

Ahora sabemos lo suficiente sobre genéricos como para abordar las funciones lambda.