W3docs

Restrições dos Generics em Java

O que você não pode fazer com generics em Java: sem primitivos, sem parâmetros de tipo estáticos, sem arrays genéricos e mais.

Este capítulo é o catálogo de coisas que você não pode fazer com generics em Java, com uma breve explicação de por que cada uma é proibida. Quase toda restrição remonta a um único fato que você viu no capítulo anterior: o parâmetro de tipo é apagado antes de o bytecode ser emitido, então qualquer coisa que precise que o parâmetro exista em tempo de execução não funcionará. Leia este capítulo como o cartão de referência final para a parte — é a lista de momentos "tentei, o compilador reclamou" condensada em uma única página.

1. Sem primitivos como argumentos de tipo

List<int> ints = new ArrayList<>();        // ❌
List<Integer> ints = new ArrayList<>();    // ✓

A forma apagada em nível de bytecode de List<E> armazena seus elementos como Object, e primitivos não são Objects. A correção é a classe wrapper correspondente — Integer, Long, Double, Boolean, Character, etc. O autoboxing então preenche a lacuna: ints.add(5) e int x = ints.get(0) funcionam ambos, com o custo de que cada elemento paga por um objeto Integer no heap.

O Project Valhalla é o esforço de longa data para fazer List<int> funcionar de verdade, por meio de value types e generics especializados. No Java 25 isso ainda não chegou.

2. Sem new T()

public class Box<T> {
  public T newInstance() { return new T(); }      // ❌
}

Em tempo de execução, não há T — a JVM tem apenas Object (ou o bound). Ela não tem objeto de classe para chamar um construtor, nem como saber qual construtor chamar. A solução padrão é receber uma factory como parâmetro:

public class Box<T> {
  public T newInstance(Supplier<T> factory) { return factory.get(); }
}

Box<String> b = new Box<>();
String fresh = b.newInstance(String::new);

O Supplier<T> carrega a factory real em tempo de execução, de uma forma que o parâmetro de tipo nunca poderia.

3. Sem T.class ou instanceof T

public <T> boolean isIt(Object o) {
  return o instanceof T;        // ❌
}

public <T> Class<T> klass() {
  return T.class;               // ❌
}

Novamente, sem T em tempo de execução. A solução nos dois casos é passar o token Class<T> como argumento:

public <T> boolean isIt(Object o, Class<T> type) {
  return type.isInstance(o);
}

Class.isInstance(Object) é a forma reflexiva de instanceof, e funciona com o objeto Class de tempo de execução que você passou. A biblioteca padrão faz isso em vários lugares — Collections.checkedList(List<E>, Class<E>), EnumSet.noneOf(Class<E>), desserializadores JSON, e assim por diante.

4. Sem arrays de um tipo genérico

T[] arr = new T[10];              // ❌ — generic array creation
List<String>[] lists = new List<String>[10];   // ❌ — same

Este é mais sutil. Arrays em Java são reificados — um Integer[] sabe em tempo de execução que é um Integer[], e as atribuições são verificadas. Mas generics são apagados — a JVM não consegue distinguir List<String>[] de List<Integer>[]. Se as duas restrições não fossem impostas, você poderia corromper o heap com poucas linhas:

List<String>[] strs = new List<String>[1];   // pretend this is legal
Object[] objs = strs;                         // arrays are covariant
objs[0] = List.of(42);                        // stores an Integer list
String s = strs[0].get(0);                    // KABOOM

O compilador recusa a criação de arrays genéricos em vez de deixar isso acontecer.

Soluções alternativas:

  • Use (T[]) new Object[n] com um @SuppressWarnings("unchecked") (você viu isso na Stack genérica anteriormente). Seguro se o array é interno e você nunca o deixa vazar como T[].
  • Ou simplesmente use um List<T> em vez de um array. Em nove de cada dez casos essa é a resposta certa.

5. Sem campos estáticos de um parâmetro de tipo

public class Box<T> {
  private static T defaultValue;       // ❌
  public  static T empty() { ... }     // ❌
}

O parâmetro de tipo pertence a uma instância — cada Box<...> carrega seu próprio T. Membros estáticos pertencem à classe em si, que não tem T. Os dois escopos não se conectam.

Se você precisa de um método estático polimórfico em um tipo, declare seu próprio parâmetro de tipo (cobrimos isso em métodos genéricos):

public class Box<T> {
  public static <U> Box<U> empty() { return new Box<>(null); }
}

<U> é local ao método — independente de qualquer T no nível da classe.

6. Sem tipos de exceção genéricos

public class MyException<T> extends Exception { ... }    // ❌

As tabelas de tratamento de exceções da JVM buscam blocos catch pelo tipo apagado. Se dois tipos de exceção genéricos diferentes fossem apagados para a mesma classe, um catch (MyException<String> e) também capturaria um MyException<Integer> — o que corromperia silenciosamente o sistema de tipos. Em vez de tentar fazer isso funcionar, Java proíbe a declaração completamente. Você também não pode ter um parâmetro de tipo genérico em uma cláusula catch:

try { ... } catch (T e) { ... }                          // ❌

Se sua exceção genuinamente precisa carregar um payload tipado, armazene o payload como um campo genérico em uma exceção não genérica:

public class TaggedException extends Exception {
  public final Object payload;
  public TaggedException(String message, Object payload) {
    super(message);
    this.payload = payload;
  }
}

Ou declare o site de lançamento de forma estreita e reserve payloads tipados para caminhos de retorno normais.

7. Sem sobrecargas que diferem apenas nos parâmetros genéricos

public void process(List<String> list)  { ... }
public void process(List<Integer> list) { ... }    // ❌ — both erase to process(List)

Após o apagamento, ambos os métodos têm assinatura process(List). Java trata a resolução de sobrecarga por assinaturas apagadas, então não consegue distinguir os dois. A solução é dar nomes diferentes a eles — processStrings e processInts — ou aceitar um List<Object> e verificar em tempo de execução.

8. Tipos genéricos são invariantes

Esta não é uma regra de "o compilador rejeita isso", mas uma regra de "o compilador rejeita o que você esperava ser legal", e a cobrimos em detalhes em Wildcards:

List<Integer> ints = ...;
List<Number>  nums = ints;       // ❌ — generic types are invariant
List<? extends Number> nums = ints;   // ✓ — wildcard restores the flexibility

Vale saber porque é a restrição com que você mais vai se deparar. Wildcards são a válvula de alívio.

9. Sem tipos de enum genéricos

public enum Box<T> {                                   // ❌
  EMPTY, FULL;
  T value;
}

Enums são traduzidos para uma única classe com um conjunto fixo de constantes — não há como as constantes compartilharem um único T sensato. A correção geralmente é tornar os métodos genéricos, não o enum em si:

public enum Box {
  EMPTY, FULL;
  public <T> T orDefault(T fallback) { return this == FULL ? null : fallback; }
}

Ou, se cada constante realmente precisa de seu próprio tipo, use uma hierarquia de classes não enum e um Map<Name, Box>.

10. Chamando um método genérico por meio de um tipo raw

List rawList = new ArrayList();
rawList.add("hi");                  // unchecked-warning, but allowed
List<String> typed = rawList;       // unchecked-warning, dangerous

Misturar tipos raw e genéricos desativa todas as verificações em tempo de compilação dos generics para aquela variável. O compilador avisará (Unchecked call to add(E) as a member of raw type java.util.List), e ignorar o aviso devolve a você a armadilha do pré-Java-5 — valores de tipo errado infiltrados silenciosamente, explodindo na próxima leitura.

Tipos raw existem para compatibilidade retroativa, não como uma funcionalidade. Trate o aviso como erro em qualquer código novo.

Um exemplo prático: cada restrição, lado a lado

O programa abaixo tenta fazer cada uma das coisas que as regras proíbem (comentadas para que o arquivo compile), depois mostra a solução canônica para cada uma. Leia os comentários — eles mapeiam um a um para as restrições numeradas acima.

java— editable, runs on the server

Cada restrição numerada mapeia para uma solução de uma linha no programa. O padrão em todas elas é o mesmo: qualquer coisa que queira o parâmetro de tipo em tempo de execução recebe a informação explicitamente — um token Class<T>, um Supplier<T>, um parâmetro de tipo em nível de método, um wildcard. O apagamento removeu a forma implícita; você a restitui na fronteira da API.

Isso encerra a Parte 10

Generics é o recurso individual mais profundo da linguagem fora da própria JVM. Agora você tem o vocabulário funcional: parâmetros de tipo em classes, métodos e interfaces; bounds; wildcards e PECS; apagamento; e o catálogo de restrições que o apagamento impõe ao design. Toda API Java moderna é moldada por essas regras, e ler código de bibliotecas (ou projetar o seu próprio) é muito mais fácil com esse modelo na cabeça.

O que vem a seguir

Generics não são um fim em si mesmos — existem porque Java precisava de uma maneira de expressar "contêiner de T" sem copiar e colar uma classe por tipo de elemento. A próxima parte do livro é o lugar para o qual toda a maquinaria genérica desta parte estava secretamente te preparando: o Collections Framework. List, Set, Map, Queue, e as dezenas de implementações por trás deles — todos parametrizados, todos projetados em torno das regras que você acabou de aprender. Continue para Introdução às Collections em Java.

Prática

Prática
Você quer um método genérico que retorne `true` quando seu argumento for uma instância de `T`. Você escreve `<T> boolean isIt(Object o) { return o instanceof T; }`. O compilador rejeita. Qual é a correção padrão?
Você quer um método genérico que retorne `true` quando seu argumento for uma instância de `T`. Você escreve `<T> boolean isIt(Object o) { return o instanceof T; }`. O compilador rejeita. Qual é a correção padrão?
Was this page helpful?