Por que os generics existem
Um tipo genérico permite escrever o código uma vez e reutilizá-lo para muitos tipos sem abrir mão da segurança de tipos. Em vez de escrever uma IntBox, uma StringBox e uma UserBox separadas, você escreve uma única Box<T> na qual T é um espaço reservado que quem chama preenche.
Você já usou generics toda vez que escreveu ArrayList<String> ou HashMap<String, Integer>. A parte <...> é um argumento de tipo. Esta página mostra como escrever os seus próprios.
A alternativa - armazenar tudo como Object - joga fora a informação de tipo e força conversões feias e propensas a erro:
Essa conversão na última linha lança uma ClassCastException em tempo de execução - exatamente o tipo de bug que os generics foram projetados para tornar impossível.
Uma classe genérica
Declare um parâmetro de tipo entre colchetes angulares após o nome da classe. Por convenção, é uma única letra maiúscula: T de "type" (tipo), E de "element" (elemento), K/V para chave/valor.
Dentro de Box, cada T se torna o que quer que quem chama tenha fornecido. Box<String> é uma caixa que só guarda e retorna String. O compilador rejeita name.set(99) antes mesmo de o programa rodar.
Os <> vazios à direita (o operador diamante) permitem que o compilador infira o argumento de tipo a partir do lado esquerdo, então você não repete <String> duas vezes.
Métodos genéricos
Um único método pode ter o seu próprio parâmetro de tipo, independente da classe. Coloque o parâmetro <T> antes do tipo de retorno:
Você nunca passa T explicitamente - o compilador o infere a partir do argumento. Métodos genéricos são como utilitários como Collections.sort ou List.of permanecem seguros em termos de tipo com qualquer tipo de elemento.
Parâmetros de tipo limitados
Às vezes um tipo genérico só faz sentido para alguns tipos. extends restringe o parâmetro para que você possa chamar os métodos do limite. Aqui T extends Number significa que T é Number ou qualquer subclasse (Integer, Double, ...), então doubleValue() fica disponível:
Observe que extends aqui significa "é um subtipo de", e funciona tanto para classes quanto para interfaces - <T extends Comparable<T>> é extremamente comum quando você precisa comparar elementos.
Curingas: ? extends e ? super
Uma sutileza traiçoeira: List<Integer> não é uma List<Number>, mesmo que Integer seja um Number. Os generics são invariantes. Os curingas relaxam isso quando você só precisa ler ou só precisa escrever.
Use ? extends T para um produtor do qual você lê, e ? super T para um consumidor no qual você escreve (a regra "PECS" - Producer Extends, Consumer Super):
Uma lista ? extends Number deixa você ler os elementos como Number, mas não adicionar a ela (o compilador não tem como saber o tipo exato do elemento). Uma lista ? super Integer deixa você adicionar Integer, mas as leituras voltam como Object. Escolha o curinga que combina com a forma como os dados fluem.
Apagamento de tipos e seus limites
Os generics são um recurso de tempo de compilação. Depois da compilação, o parâmetro de tipo é apagado - em tempo de execução Box<String> e Box<Integer> são ambas apenas Box. Isso mantém os generics compatíveis com código antigo, mas impõe limites reais.
// Nenhum destes compila - o parâmetro de tipo não existe em tempo de execução:
T value = new T(); // não dá para instanciar um parâmetro de tipo
T[] array = new T[10]; // não dá para criar um array genérico
if (list instanceof List<String>) { } // não dá para testar o argumento de tipo
Como o tipo desaparece em tempo de execução, você não pode perguntar "o que era T?" via reflexão, e não pode sobrecarregar métodos que diferem apenas pelo seu argumento genérico (foo(List<String>) e foo(List<Integer>) são apagados para a mesma assinatura). Quando você realmente precisa do tipo em tempo de execução, passe um token Class<T> como parâmetro do construtor ou do método.
Próximo: expressões lambda
Você viu que um método genérico recebe um tipo como parâmetro. O próximo passo é tratar o comportamento como parâmetro. As expressões lambda permitem passar um trecho de código - uma função - para um método, que é exatamente como você vai ordenar, filtrar e transformar as coleções genéricas que acabou de aprender a tipar com segurança.
Perguntas frequentes
O que são generics em Java?
Os generics permitem escrever uma classe ou método que funciona com um tipo que você especifica depois, em vez de prendê-lo a um único tipo concreto. Você declara um parâmetro de tipo entre colchetes angulares - class Box<T> - e quem chama preenche o tipo real - Box<String>. O compilador então faz valer esse tipo em toda parte, então você detecta incompatibilidades em tempo de compilação e dispensa conversões manuais.
Por que usar generics em vez de Object?
Usar Object perde toda a informação de tipo: o compilador não consegue impedir que você coloque algo errado, e você precisa converter cada valor que sai (com risco de ClassCastException em tempo de execução). Os generics empurram essa verificação para o tempo de compilação. Uma List<String> simplesmente não aceita um Integer, e get() já retorna uma String - sem conversão, sem surpresa em tempo de execução.
O que é apagamento de tipos nos generics de Java?
Apagamento de tipos significa que a informação do tipo genérico existe apenas em tempo de compilação. Depois de compilar, List<String> e List<Integer> são ambas apenas List em tempo de execução - o parâmetro de tipo é apagado. É por isso que você não pode escrever new T[10], chamar list instanceof List<String> nem ler um parâmetro de tipo via reflexão. Os generics oferecem segurança em tempo de compilação, não dados de tipo em tempo de execução.