Introdução aos Generics do Java
Por que existem generics no Java — segurança de tipos, reutilização de código e eliminação de casts em coleções e APIs.
Generics são o recurso que permite que uma classe, interface ou método opere sobre um tipo não especificado, e então faz com que o compilador fixe esse tipo no ponto em que você o usa. Uma List<String> é uma lista de strings, o compilador sabe disso, e qualquer tentativa de inserir um Date nela é rejeitada antes mesmo do programa executar. Antes de os generics chegarem no Java 5, a mesma lista era uma List de Object, e toda leitura dela exigia um cast manual que poderia ou não ter sucesso em tempo de execução. Os generics transformaram essa aposta em tempo de execução em uma verificação em tempo de compilação, e praticamente toda API Java moderna é moldada por eles.
O problema que os generics resolvem
Para entender por que os generics existem, imagine um Java sem eles. Um contêiner que armazena coisas arbitrárias precisa declarar seus conteúdos como Object:
// Pre-Java-5 style — what the standard library actually looked like.
List names = new ArrayList();
names.add("Ada");
names.add("Linus");
String first = (String) names.get(0); // cast required, never checked by the compilerDois problemas. Primeiro, o cast é ruído — toda leitura do contêiner precisa de um. Segundo, e pior ainda, nada impede alguém de inserir um Date nessa mesma lista:
names.add(new java.util.Date()); // compiler is fine with this
String oops = (String) names.get(2); // ClassCastException at runtimeO bug aparece na leitura, longe da escrita. O cast mente — ele diz "isto é uma String," e a JVM descobre só quando já é tarde demais para fornecer um stack frame útil próximo à falha real.
Os generics corrigem os dois problemas:
List<String> names = new ArrayList<>();
names.add("Ada");
names.add("Linus");
names.add(new Date()); // ❌ compile error — won't even build
String first = names.get(0); // no cast — the compiler already knows it's a StringO <String> entre colchetes angulares é o parâmetro de tipo. Ele informa ao compilador "esta lista contém Strings," e a partir desse momento toda chamada a add e get é verificada em relação a essa promessa.
Três benefícios que você obtém gratuitamente
Os generics oferecem três benefícios concretos, e são a razão pela qual toda coleção, stream e optional no JDK moderno é genérico:
- Verificações mais fortes em tempo de compilação. A inserção de tipo errado acima é detectada no momento do build, não em produção. Uma classe de
ClassCastExceptionsimplesmente para de acontecer. - Sem mais casts. Ler de um
Map<String, User>fornece umUser, não umObjectque você precisa converter. Menos ruído sintático, menos para ler, menos para manter. - Reutilização de código sem copiar e colar. Uma classe
List<E>funciona para todo tipo de elemento. Antes dos generics, a biblioteca padrão ou aceitavaObjectem todo lugar, ou entregava umaStringList,IntList,DateList, e assim por diante. Agora você escreve uma classe e deixa o chamador parametrizá-la.
Esse último ponto é a maior vitória arquitetural. Generics são a forma de escrever um contêiner, um algoritmo ou um formato de callback uma vez e fazê-lo se aplicar a todo tipo que o chamador possa passar.
Uma primeira classe genérica
A convenção é nomear um parâmetro de tipo com uma única letra maiúscula — T para um "tipo" genérico, E para "elemento" de uma coleção, K/V para "chave" e "valor" de um mapa, R para "retorno". Aqui está a classe genérica mais simples possível — um par de duas coisas do mesmo tipo:
public class Pair<T> {
private final T first;
private final T second;
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
public T first() { return first; }
public T second() { return second; }
}O <T> após o nome da classe introduz o parâmetro de tipo. A partir daí, T pode ser usado dentro da classe em qualquer lugar onde um tipo normal poderia aparecer. O chamador escolhe T ao criar o objeto:
Pair<String> names = new Pair<>("Ada", "Grace");
Pair<Integer> scores = new Pair<>(100, 87);
String n1 = names.first(); // already a String, no cast
int s1 = scores.first(); // auto-unboxed from IntegerO <> vazio à direita (o operador diamante, Java 7+) instrui o compilador a inferir o tipo a partir da declaração do lado esquerdo — você quase nunca precisa repetir o argumento de tipo.
O que pode ser parametrizado, e o que não pode
Um parâmetro de tipo pode substituir:
- O tipo de um campo (
private T value;) - Um parâmetro ou tipo de retorno de um método (
public T get() { ... },void put(T value)) - O tipo de elemento de um array desse tipo (
T[] items— com algumas ressalvas)
Um parâmetro de tipo não pode substituir:
- Um primitivo (
Pair<int>é ilegal — usePair<Integer>e deixe o autoboxing fazer o trabalho) - O parâmetro de tipo de um campo estático ou método estático (o parâmetro pertence a uma instância, não à própria classe)
- O alvo de
new T()ouinstanceof T— o Java apaga os generics em tempo de execução, então o programa não tem umTpara construir ou testar
A lista completa de "coisas que você não pode fazer" tem seu próprio capítulo no final desta parte — Java Generics Restrictions — uma vez que cobrirmos o suficiente da mecânica para que as regras façam sentido.
Um exemplo trabalhado: segurança de tipos vs. tipos brutos, lado a lado
O programa abaixo constrói o mesmo contêiner duas vezes — uma vez como um List bruto (o formato pré-generics) e outra como um List<String>. Ambos compilam; apenas o parametrizado é seguro.
A versão bruta trava no meio da iteração porque o loop confiou em um cast que não tinha razão de confiar. A versão genérica tornou o mesmo erro irrepresentável — o add(42) inválido nem compila. Essa mudança de tempo de execução para tempo de compilação é a razão pela qual os generics existem.
O que esta parte do livro cobre
Os capítulos restantes nesta parte desmontam os generics peça por peça:
- Classes genéricas — o parâmetro de tipo no nível de classe que você acabou de ver, com mais profundidade.
- Métodos genéricos — métodos que introduzem seu próprio parâmetro de tipo, independente da classe.
- Interfaces genéricas — projetando contratos de API parametrizados por um tipo.
- Parâmetros de tipo delimitados — dizendo "T deve estender
Number" para que você possa chamar métodos emT. - Wildcards —
? extends T,? super T, e a regra PECS que decide quando usar cada um. - Type erasure — como a JVM implementa generics internamente, e por que algumas coisas que você esperaria funcionar não funcionam.
- Restrições — o catálogo de coisas que a linguagem se recusa a deixar você fazer, com as razões por trás de cada uma.
Leia-os em ordem — cada capítulo pressupõe os anteriores.
O que vem a seguir
Comece com o formato mais comum — uma classe cujos campos e métodos são parametrizados por um tipo que o chamador escolhe. Continue em Java Generic Classes.