Skip to content

3. As assinaturas das classes e métodos genéricos

  

A biblioteca RxJava possui vários métodos que aceitam como parâmetros instâncias de interfaces genéricas. Às vezes, a assinatura dessas interfaces é complexa. Aqui estão alguns exemplos:


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)

Esses dois métodos utilizam dois tipos genéricos, T e R. Mas o que significa, por exemplo, a definição do parâmetro do método map: Func1<? super T,? extends R> func ?

Informações sobre genéricos podem ser encontradas no URL [https://docs.oracle.com/javase/tutorial/java/generics/]. Algumas informações a seguir têm origem neste URL. Os genéricos surgiram com a versão 1.5 do Java.

Imaginemos um serviço web que fornece informações de diferentes tipos. O serviço web pode, às vezes, não conseguir fornecer essas informações e, nesse caso, deve sinalizar um erro ao seu cliente. É possível, então, padronizar a resposta do serviço web da seguinte forma:


public class Response<T> {

    // ----------------- propriedades
    // status da operação
    private int status;
    // eventuais mensagens de erro
    private List<String> messages;
    // o corpo da resposta
    private T body;
...
}
  • linha 1: a classe [Response] é parametrizada pelo tipo T;
  • linha 9: o tipo T é o tipo do corpo da resposta, que é o que o cliente realmente espera;
  • linha 5: um código de erro, 0 se não houver erro;
  • linha 7: mensagens explicando o erro, null se não houver erro;

Do lado do cliente, poderemos então escrever:


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

ou


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

O tipo formal T é substituído por um tipo efetivo, neste caso [Product] ou [List<Product>]. A classe genérica [Response<T>] se mostra prática aqui, pois permite trabalhar com uma resposta padrão.

Para criar um tipo [Response] com o operador new, escrever-se-á:


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

A partir da versão 1.8 do Java, a instrução anterior pode ser escrita de forma mais simples:


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

Agora não é mais necessário especificar, à direita do sinal =, o tipo efetivo dos parâmetros genéricos: será utilizado o tipo formal especificado à esquerda do sinal =. Isso é chamado de inferência de tipo: o compilador é capaz de determinar por conta própria o tipo efetivo dos parâmetros genéricos com base no contexto. Essa característica é amplamente utilizada em funções lambda, nas quais o tipo efetivo dos parâmetros genéricos de um método é frequentemente omitido.

Agora, no lado do cliente, o método [getDataFromWebService] poderia ter a seguinte assinatura:


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

Trata-se de um método genérico parametrizado por dois tipos, T1 e T2:

  • T1 é o tipo do corpo da resposta esperada;
  • T2 é o tipo do valor enviado para uma operação POST; null para uma operação GET;
  • urlWebService: é o URL do serviço web;
  • httpMethod: é o método HTTP, GET ou POST a ser utilizado para consultar este URL;

Do lado do cliente, poderemos ter a seguinte chamada:


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

Nesse caso, teremos T1 = Product e T2 = Long.

Com o Java 8, é possível usar a inferência de tipos e escrever de forma mais simples:


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

Aqui está outra forma possível de chamada:


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

Nesse caso, teremos T1 = List<Product> e T2 = Long[].

A classe ou o método genérico podem impor restrições aos seus parâmetros genéricos. Voltemos à definição do método [map] de RxJava:


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

O método aceita dois tipos de parâmetros, T e R. O tipo [Func1] é uma interface genérica:

 

Func1 define uma interface funcional, ou seja, uma interface que possui apenas um único método. As interfaces funcionais são a base das funções lambda do Java 8. Aqui, o método da interface é definido como:

R call(T t)

Portanto, T é o tipo do parâmetro passado para [call] e R é o tipo do resultado obtido. Voltemos à definição do método [map]:


public final <R> Observable<R> map(Func1<? super T,? extends R> func)
  • o método [map] espera um único parâmetro do tipo [Func1<? super T,? extends R>] e retorna um tipo [Observable<R>];
  • ? super T: o parâmetro passado ao método [Func1.call] deve ser do tipo T ou um tipo pai de T;
  • ? extends R: o tipo do resultado retornado pelo método [call] deve ser do tipo R ou derivado, se R for uma classe, ou implementar R, se R for uma interface;

Para entender melhor as duas restrições anteriores, vejamos alguns exemplos extraídos da referência [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("algum texto"); // erro: isso ainda é um String!
    }
}
  • linha 3: a classe [Box] é parametrizada pelo tipo T, que é o tipo do campo da linha 5;
  • linha 15: o método [inspect] aceita um parâmetro do tipo U. Um método também pode ter parâmetros genéricos. Nesse caso, ele é declarado da seguinte forma:
<T1, T2, ..., Tn> TResult méthode(paramètres ...)

Aqui, U é declarado como tipo genérico do método por <U>, mas com uma restrição <U extends Number>, ou seja, U deve estender a classe [Number]. Devido a essa restrição, o compilador sinaliza um erro na linha 25;

  • linhas 23-24: chamadas ao método [inspect] com tipos derivados de [Number];

Obtêm-se os seguintes resultados:

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

Observação: para executar a classe no IntelliJ, siga estes passos para [1, 2]:

 

Agora, vamos considerar o seguinte exemplo:


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

O método [addNumbers] aceita como parâmetro um tipo List<T>, em que T é uma classe pai da classe [Integer]. Devido a essa restrição, os inteiros de 1 a 3 (int) podem ser adicionados à lista (linhas 2 a 4) e o método poderia ser chamado da seguinte maneira:


    List<Number> numbers=new ArrayList<>();
    // adição de um número duplo <-- Double
    numbers.add(7.8);
    // adição de um Long a um Number <-- Long
    numbers.add(1L);
    // adição de List<Number> a Number <-- Integer
    addNumbers(numbers);
    // exibição de List<Number>
    for(Number number : numbers){
      System.out.println(number.getClass().getName());
}

Obtém-se, então, o seguinte resultado:

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

Para estender a classe Box&lt;T&gt;, escreveremos:


class OtherBox<T> extends Box<T> {

}

A classe filha pode, por sua vez, introduzir novos parâmetros genéricos:


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

Uma interface também pode utilizar parâmetros genéricos:


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

Para implementá-la, utiliza-se a seguinte sintaxe:


class A<T> implements I<T> {

    @Override
    public void doSomethingWith(T t) {
        System.out.printf("%s%n", t);
    }

}

Agora já sabemos o suficiente sobre genéricos para abordar as funções lambda.