W3docs

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 compiler

Dois 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 runtime

O 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 String

O <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 ClassCastException simplesmente para de acontecer.
  • Sem mais casts. Ler de um Map<String, User> fornece um User, não um Object que 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 aceitava Object em todo lugar, ou entregava uma StringList, 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 Integer

O <> 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 — use Pair<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() ou instanceof T — o Java apaga os generics em tempo de execução, então o programa não tem um T para 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.

java— editable, runs on the server

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 em T.
  • 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.

Prática

Prática
Um método declara `public static List getNames() { ... }` (sem parâmetro de tipo na lista). O chamador escreve `String first = getNames().get(0);`. Por que o compilador avisa — e qual é o perigo se você ignorar o aviso?
Um método declara `public static List getNames() { ... }` (sem parâmetro de tipo na lista). O chamador escreve `String first = getNames().get(0);`. Por que o compilador avisa — e qual é o perigo se você ignorar o aviso?
Was this page helpful?