Genéricos, também conhecidos como Tipos Genéricos, são parâmetros de tipos em classes, métodos e interfaces. É possível definir tipos genéricos para as variáveis de referência dentro de um determinado escopo (classes, interfaces e métodos) e ao realizar uma chamada ao método, ou definir um variável de referência e/ou instanciar um construtor os tipos genéricos são substituídos pelo tipo especificado.

Com a utilização de genéricos é possível abstrair o tipo de parâmetro e/ou de retorno de métodos, o que garante maior reaproveitamento de código.

Parâmetro de tipo

editar

Como visto no capítulo sobre Collections é possível definir o tipo do elemento que será armazenado e recuperado de uma coleção ao especificar-se na variável entre um par de chevrons, < e >, o tipo desejado. Esse tipo utilizado nas variáveis da lista de parâmetros e no tipo de retorno dos métodos é chamado de tipo genérico.

Na documentação java.util.ArrayList há as seguintes assinaturas de método que utilizam o tipo genérico E.

public boolean add(E e)
public E get(int index)

É convencionado o identificador E para elementos, K para chaves, V para valores e T para tipos, porém não há obrigatoriedade no uso. Pode-se utilizar qualquer identificador válido no parâmetro de tipo.

Parâmetro de tipo é o tipo genérico definido entre o par de chevrons na definição de uma classe ou interface. Deve ser inserido após o identificador da classe. No código abaixo temos a definição da classe java.util.ArrayList e da interface java.util.List, ambas com o parâmetro de tipo definidos com o tipo genérico E.

//Declarações parciais para evidenciar apenas os conceitos apresentados.
class ArrayList<E> {...} 

interface List<E> {...}

Argumento de tipo é o tipo especificado, entre o par de chevrons, na declaração de um tipo (chamada de construtor ou variável de referência) que substituirá o tipo genérico. No código abaixo o tipo genérico E foi substituído, em tempo de compilação, pelo argumento de tipo String. Com isso o tipo da variável na lista de parâmetros do método boolean add(E e) foi alterado de E e para String e e o tipo de retorno do método E get(int index) foi de E para String.

List<String> lista = new ArrayList<String>();
lista.add("Elemento");
String elemento = lista.get(0);
System.out.println(elemento);
Elemento

Para declarar-se classe ou interface com referências de tipos genéricos, basta especificar o tipo a ser substituído no parâmetro de tipo.

class Objeto<T> {
    T objeto;

    T getObjeto() {
        return objeto;
    }

    void setObjeto(T objeto) {
        this.objeto = objeto;
    }
}

Ao utilizar-se uma classe ou interface que utiliza tipos genéricos basta enviar no argumento de tipo o tipo desejado. No código abaixo String foi definido como o tipo na variável de referência (Objeto<String> obj) e na chamada do construtor (new Objeto<String>()).

Objeto<String> obj = new Objeto<String>();
obj.setObjeto("Uma string qualquer.");
System.out.println(obj.getObjeto());
Uma string qualquer.

Pode-se definir mais de um tipo de parâmetro. Para tal, é necessário separá-los com vírgula e não utilizar o mesmo tipo já utilizado.

class Par<K, T> {
    K chave;    
    T objeto;    
    
    Par(K chave, T objeto){
        this.chave = chave;
        this.objeto = objeto;
    }

    T getObjeto() {
        return objeto;
    }
    
    K getChave() {
    	return chave;
    }

    void setObjeto(T objeto) {
        this.objeto = objeto;
    }
}

class Teste {
    public static void main(String[] args) {
        Par<Integer, String> par = new Par<Integer, String>(42, "É a pergunta?");
        par.setObjeto("é a resposta para o universo.");
        System.out.print(par.getChave() + " " + par.getObjeto() + "\n");
    }
}
42 é a resposta para o universo.

A partir do Java SE 7 pode-se utilizar o operador diamante, que nada mais é que o argumento de tipo sem tipo especificado na chamada do construtor. Em tempo de compilação o argumento de tipo do construtor será inserido em correspondência ao argumento de tipo do tipo da variável.

Par<Integer, String> teste = new Par<>();

É possível definir uma classe ou interface com tipos genéricos em parâmetros de tipo.

Par<Integer, Objeto<String>> par = new Par<>(52, new Objeto<>());
par.getObjeto().setObjeto("objeto");
System.out.println(par.getChave() + ": " + par.getObjeto().getObjeto() + "\n");
52: objeto

É possível repassar o parâmetro de tipo definido em uma classe como argumento de tipo para referências de tipo genérico. No código abaixo a classe Colecao foi definida com o tipo T em seu parâmetro de tipo. O tipo T é então utilizado como argumento de tipo da variável de referência List<T> lista.

class Colecao<T> {
    List<T> lista;

    List<T> getLista() {
        return lista;
    }

    void setLista(List<T> lista) {
        this.lista = lista;
    }
}

class Teste {
    public static void main(String[] args) {
        Colecao<Double> colecao = new Colecao<>();
        colecao.setLista(Arrays.asList(5.4, 3.43));
        System.out.println(colecao.getLista());
    }
}
[5.4, 3.43]

Parâmetro de tipo limitado

editar

É possível substituir T por java.lang.Object nos códigos da subseção anterior, logo, a única vantagem é a eliminação de cast no retorno de uma chamada ao método getObjeto.

Um benefício maior na utilização de Genéricos se dá ao especificar-se uma restrição aos tipos de parâmetro possíveis. É possível obter essa restrição definindo T como subclasse de determinada classe ou como implementador de determinada interface com a utilização da instrução extends.

No código abaixo, T é restringido à CharSequence ou suas classes implementadoras.

class Teste<T extends CharSequence> {
    public static void main(String[] args) {
        Teste<String> teste = new Teste<>();
        String palavra = teste.metodo("Exemplo");
	System.out.println(palavra);
    }

    T metodo (T t) {
        System.out.println(t.subSequence(0, 2));
	return t;
    }
}
Ex
Exemplo

Caso utilizado um tipo diferente na linha 7, como metodo(5); teria-se o erro de compilação abaixo.

Teste.java:7: error: method metodo in class Teste cannot be applied to given types;
    String palavra = metodo(5);
                     ^
  required: T
  found: int
  reason: inferred type does not conform to upper bound(s)
    inferred: Integer
    upper bound(s): CharSequence
  where T is a type-variable:
    T extends CharSequence declared in method <T>metodo(T)
1 error

Inferência de tipo em métodos

editar

Alternativa à inferência de tipo no parâmetro de tipo da classe, é possível definir um parâmetro de tipo apenas para um método.

Para isso basta inserir o parâmetro de tipo antes do tipo de retorno.

<T> void metodo () {}

No código abaixo o tipo genérico, definido como T, é utilizado no tipo de retorno e no tipo do parâmetro na lista de parâmetros da declaração de método. Em tempo de compilação, o tipo T será convertido no tipo do argumento do método chamador, no caso abaixo como o argumento do método é do tipo String ("Exemplo") logo o tipo definido será String.

class Teste {
    public static void main(String[] args) {
        String palavra = Teste.metodo("Exemplo");
        System.out.println(palavra);
    }

    static <T> T metodo (T t) {
        return t;
    }
}
Exemplo

Tipo Cru

editar

Para manter-se a compatibilidade com versões anteriores ao Java SE 5, a partir do lançamento da referida versão foi permitido que argumentos de tipo possam ser omitidos, são os chamados Tipos Crus.

//Tipo parametrizado.
Set<Double> conjunto;

//Tipo cru.
Set conjunto;

Em tempo de compilação os tipos crus são convertidos para o tipo Object.

List lista = new ArrayList();
lista.add(5.5);
int i = (int) lista.get(0);
System.out.println(i);
5.5


Caso não houvesse o cast (int) na linha 9 o seguinte erro seria exibido.

Teste.java:9: error: incompatible types: Object cannot be converted to int
       int i = lista.get(0);
                        ^
Note: Teste.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.

Exercícios

editar