Como utilizar tipos genéricos

Este tutorial visa demonstrar a utilização de tipos genéricos como cliente. Para declarar tipos genéricos verifique este post (ainda não publicado :D). Java SE 10 é a versão mínima para executar os códigos contidos neste post.

Definições iniciais

Tipo genérico é uma classe ou interface que após seu nome declara entre <> uma lista de parâmetros de tipo. List<E>  é um tipo genérico (mais especificamente uma interface genérica). Seu parâmetro de tipo é E . TreeMap<K,V>  é um classe genérica, com K  e V  sendo seus parâmetros de tipo. Método genérico é um método que introduz uma lista de parâmetros de tipo. Seus parâmetros de tipo são declarados entre <> antes de seu tipo de retorno. public static final <T> Set<T> emptySet() é um método genérico com T como seu parâmetro de tipo e  Set<T>  como seu tipo de retorno. Parâmetros de tipo são tipos destinados a serem substituídos por argumentos de tipo ou inferência. Costumeiramente são nomeados com apenas uma letra maiúscula. Variáveis de tipo são referências aos parâmetros de tipo nos corpos de classes, interfaces, métodos ou construtores. Tipo parametrizado é um tipo genérico definido com argumentos de tipo. List<String> é um tipo parametrizado composto pelo tipo genérico List<E> com String sendo enviada como argumento de tipo para o parâmetro de tipo E.

Parametrização de tipo

Com a parametrização de tipo é possível definir tipos de forma similar ao processo de definir valores nos parâmetros de métodos e construtores com o envio de argumentos. A classe ArrayList<E>  declara E como parâmetro de tipo. E representa o tipo dos elementos contidos nessa coleção. Quando um tipo é enviado como argumento de tipo, todas as referências à E são substituídas durante a compilação para o tipo enviado. Na linha 7 a classe LinkedList<E> tem seu parâmetro de tipo E definido como Character. Com isso, todas as variáveis de tipo E contidas no corpo de LinkedList serão substituídas por  Character. Nas linhas 8-9 o tipo  E do parâmetro e no método boolean add(E e) é efetivamente considerado como Character devido a parametrização de tipo ocorrida na linha 7. Nas linha 10-11 os métodos E removeFirst() e E getLast() devolvem referências à Character pois a variável de tipo E, tipo de retorno de ambos, foi definida como Character na linha 7.

Limitações

Tipos primitivos são ilegais na parametrização de parâmetros.

Supertipos na parametrização de tipo

As mesmas regras de cast se aplicam. O tipo enviado como argumento de tipo será definido como retorno dos métodos que utilizam o parâmetro de tipo como retorno. Na linha 8, o tipo genérico List<E> foi parametrizado como List<CharSequence> acarretando que seu parâmetro de tipo E seja definido como CharSequence. A partir dessa linha, todos os métodos que utilizam o parâmetro de tipo E terão E substituído por CharSequence. Nas linhas 9-10 o método boolean add(E e) recebe instâncias de StringStringBuilder porém em seu corpo se refere a eles como  CharSequence (superinterface de ambos). Nas linhas 11-12, o método E get(int index) devolve referência do tipo CharSequence. Na linha 12, porém, um erro de compilação ocorre pois não é possível atribuir uma instância de  CharSequence a uma referência de String sem a utilização de cast.

tipo curinga

O tipo curinga ?  na parametrização de tipo representa ‘qualquer tipo’.  ? não pode ser utilizado como parâmetro de tipo, apenas como argumento de tipo. O método void shuffle(List<?> list) da classe Collections define que qualquer parametrização de List<E>  é válida como argumento. Instâncias de ArrayList<String> e LinkedList<Object>  seriam referências válidas.

com limite superior

O tipo curinga pode ser limitado com a palavra-chave extends seguida de uma variável de tipo ou um tipo ‘regular’. ? extends T significa que qualquer tipo parametrizado com T ou um subtipo de T é válido. ? extends Number representa que qualquer tipo genérico parametrizado com Number ou um de seus subtipos ( Integer, Long, etc.) é válido. Na linha 13 o tipo do parâmetro c  do construtor LinkedList(Collection<? extends E> c)  é resolvido como Collection<? extends CharSequence> pois o argumento de tipo  CharSequence que substitui a variável de tipo E  é inferido da variável lista  (mais sobre inferência de tipo na próxima seção). A referência à Set<String>  enviada como argumento do construtor é válida dado que String é um subtipo de CharSequence.

Inferência de Tipo

Construtores

Por atribuição

O operador diamante <> nas chamadas de construtores de tipos genéricos possibilita a inferência de tipo. Os parâmetros de tipo do construtor serão automaticamente definidos com os argumentos de tipo da variável que recebe a referência criada pela chamada do construtor. No trecho acima o construtor de TreeSet<K,V>  têm seus parâmetros de tipo inferidos com Integer e Set<String>, respectivamente, devido a parametrização de tipo ocorrida na variável dicionario .

Por ARGUMENTO

Da mesma forma, uma invocação de construtor enviada como argumento de um método ou construtor têm seus parâmetros de tipos inferidos. Por exemplo, um suposto método void insiraInteiros(Set<Integer> ints) poderia ser invocado como insiraInteiros(new HashSet<>())  e a inferência de tipo definiria a chamada do construtor como HashSet<Integer>.

Métodos

Por atribuição

Tipos de retorno genéricos de métodos genéricos também podem ter seu tipo inferido quando atribuídos a variáveis ou passados como argumentos. O método public static final <T> Set<T> emptySet()  tem seu parâmetro de tipo T inferido com Character devido a parametrização de tipo na variável caracteres.

Por ARGUMENTO

Método genéricos também podem ter seus parâmetros de tipo inferidos de acordo com os tipos passados como argumento. O método static <K, V> Map<K, V> of(K k1, V v1) tem seus parâmetros de tipo inferidos como Integer  e String, respectivamente. Na resolução de inferência de tipo o compilador manterá, neste caso, o tipo mais específico dos valores enviados como argumento. No trecho de código acima, o método of  retorna uma instância de Map<Integer, String>, porém quando o método V put(K key, V value) é invocado nessa instância um erro de compilação ocorre pois é ilegal enviar um double (2.0) para um argumento definido como  Integer (key). É possível, contudo, limitar a resolução de inferência para o tipo mais específico do argumento. Utilizando a inferência por atribuição, demonstrada na seção anterior, o método of terá sua parametrização de tipo inferida de acordo com os parâmetros de tipo definidos na variável. No caso, o argumento 1 sendo referido como Number, superinterface de Integer, e “um” como CharSequence, superinferface de String.

var

Tipos genéricos atribuídos a variáveis locais var terão sempre seus parâmetros de tipos inferidos com Object, seja retorno de invocação a método genérico ou chamada de construtor genérico. Na linha 8 a chamada ao método public static final <T> List<T> emptyList() retorna uma referência do tipo List<Object> uma vez que T é inferido como Object dada a atribuição à uma variável local com a palavra-chave var. A linha 9 não compila pois o método static void insiraInteiros(List<Integer> ints) recebe referência à instância de tipo  List<Object>. O código acima produz o seguinte erro de compilação.

Referências

  • Bloch, Joshua. 2018. Effective Java Third Edition. Boston: Addison-Wesley. ISBN: 0134998065. Capítulo 5.
  • https://docs.oracle.com/javase/tutorial/java/generics/index.html
  • https://docs.oracle.com/javase/specs/jls/se11/html/jls-4.html#jls-4.4