Skip to content

3. Las firmas de las clases y métodos genéricos

  

La biblioteca RxJava cuenta con numerosos métodos que aceptan como parámetros instancias de interfaces genéricas. En ocasiones, la firma de estas es compleja. A continuación, se presentan 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, por ejemplo, la definición del parámetro del método map: Func1<? super T,? extends R> func ?

La información sobre los genéricos se puede encontrar en el URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Parte de la información que se presenta a continuación proviene de este URL. Los genéricos aparecieron con la versión 1.5 de Java.

Imaginemos un servicio web que proporciona información de diferentes tipos. En ocasiones, el servicio web puede no poder entregar dicha información y, por lo tanto, debe notificar un error a su cliente. En ese caso, se puede estandarizar la respuesta del servicio web de la siguiente manera:


public class Response<T> {

    // ----------------- propiedades
    // estado de la operación
    private int status;
    // posibles mensajes de error
    private List<String> messages;
    // el cuerpo de la respuesta
    private T body;
...
}
  • línea 1: la clase [Response] se define con el tipo T;
  • línea 9: el tipo T es el tipo del cuerpo de la respuesta, que es lo que realmente espera el cliente;
  • 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;

Del lado del cliente, entonces se podrá escribir:


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

o


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

El tipo formal T se reemplaza por un tipo efectivo, en este caso [Product] o [List<Product>]. La clase genérica [Response<T>] resulta práctica aquí, ya que permite trabajar con una respuesta estándar.

Para crear un tipo [Response] con el operador new, se escribirá:


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

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


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

Ya no es necesario especificar, a la derecha del signo =, el tipo efectivo de los parámetros genéricos: se utilizará el tipo formal especificado a la izquierda del signo =. A esto se le llama inferencia de tipos: el compilador es capaz de determinar por sí mismo el tipo efectivo de los parámetros genéricos según el contexto. Esta característica se utiliza ampliamente en las funciones lambda, donde a menudo se omite el tipo efectivo de los parámetros genéricos de un método.

Ahora, del 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, y null para una operación GET;
  • urlWebService: es el URL del servicio web;
  • httpMethod: es el método HTTP, GET o POST que se debe utilizar para consultar este URL;

Del lado del cliente, se podría 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, se puede utilizar la inferencia de tipos y se escribirá de manera más sencilla:


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

Aquí hay otra forma posible de llamar a la función:


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 el método genérico pueden imponer restricciones a sus parámetros genéricos. Volvamos a la definición del método [map] de RxJava:


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

El método admite 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 que solo tiene un método. Las interfaces funcionales son la base de las funciones lambda de Java 8. Aquí, el método de la interfaz se define como:

R call(T t)

Por lo tanto, T es el tipo del parámetro que se pasa 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 tipo padre de T;
  • ? extends R: el tipo del resultado devuelto por el método [call] debe ser de tipo R o un tipo derivado de R si R es una clase, o bien implementar R si R es una interfaz;

Para comprender mejor las dos restricciones anteriores, veamos algunos ejemplos tomados 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("algún texto"); // error: ¡esto sigue siendo una cadena!
    }
}
  • línea 3: la clase [Box] se parametriza con el tipo T, que es el tipo del campo de la línea 5;
  • línea 15: el método [inspect] admite 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 tipo genérico del método mediante <U>, pero con una restricción <U extends Number>, es decir, que U debe extender la clase [Number]. Debido a esta restricción, el compilador reporta 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, siga estos pasos para [1, 2]:

 

Ahora, consideremos el siguiente ejemplo:


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

El método [addNumbers] admite como parámetro un tipo List<T>, donde T es una clase padre de la clase [Integer]. Debido a esta restricción, los números enteros del 1 al 3 de int pueden agregarse a la lista (líneas 2-4) y el método podría invocarse de la siguiente manera:


    List<Number> numbers=new ArrayList<>();
    // se agrega un número de tipo Double <-- Double
    numbers.add(7.8);
    // agregación de un número de tipo Long <-- Long
    numbers.add(1L);
    // adición de List<Number> a Number <-- Integer
    addNumbers(numbers);
    // visualización de List<Number>
    for(Number number : numbers){
      System.out.println(number.getClass().getName());
}

De esta manera, se obtiene 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>, se escribirá:


class OtherBox<T> extends Box<T> {

}

La clase hija 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, se 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 los genéricos como para abordar las funciones lambda.