Wildcards Genéricos em Java
Use wildcards com limite superior, inferior e ilimitados em Java generics, e a regra PECS.
Um wildcard é o token ? que aparece em tipos genéricos no lugar de um argumento de tipo concreto — List<?>, List<? extends Number>, List<? super Integer>. É a resposta para um problema que você encontra quase imediatamente ao começar a escrever código genérico: List<Integer> não é um subtipo de List<Number>, mesmo que Integer seja subtipo de Number. Wildcards são a forma de descrever "uma lista de algum Number" sem se comprometer com um tipo de elemento específico — e são, de longe, a parte mais confusa do sistema de tipos do Java.
O ponto de partida contraintuitivo
Aqui está o fato que torna os wildcards necessários:
List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = ints; // ❌ does not compileMesmo que Integer extends Number, List<Integer> não estende List<Number>. Tipos genéricos são invariantes — List<Sub> e List<Super> são não relacionados, independentemente do que Sub e Super sejam.
O motivo é sólido, embora surpreendente. Se List<Integer> fosse uma List<Number>, você poderia fazer isso:
List<Number> nums = ints; // pretend this is legal
nums.add(3.14); // legal — 3.14 is a Number
int x = ints.get(3); // KABOOM at runtime — it's a DoubleO cast na última linha explodiria. Para evitar que isso aconteça, o compilador recusa logo o primeiro passo: List<Integer> não é uma List<Number>. Ponto final.
Wildcards são a forma de recuperar a flexibilidade com segurança.
O wildcard ilimitado: List<?>
O wildcard mais simples é o solitário ? — "uma lista de algum tipo desconhecido":
public static void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
}
printAll(List.of(1, 2, 3)); // List<Integer> — OK
printAll(List.of("a", "b")); // List<String> — OK
printAll(new ArrayList<>()); // List<Object> — OKDentro do corpo, a única coisa que você pode fazer com os elementos de uma List<?> é lê-los como Object — porque o compilador não tem ideia do que ? é. Você não pode adicionar nada a uma List<?> (com a única exceção de null):
public static void corrupt(List<?> list) {
list.add("hello"); // ❌ does not compile — ? is unknown
list.add(null); // ✓ — null is a value of every reference type
}List<?> é o que você escreve quando quer expressar "aceito qualquer lista e vou apenas ler dela como Object."
Wildcard com limite superior: ? extends T
Quando você precisa ler os elementos como um tipo específico — digamos, tratá-los todos como Number — use um wildcard com limite superior:
public static double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // legal — every element IS-A Number
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> — OK, Integer extends Number
sum(List.of(1.5, 2.5)); // List<Double> — OK
sum(List.of(1L, 2L, 3L)); // List<Long> — OKList<? extends Number> se lê como "uma lista de algum tipo específico que é Number ou um subtipo de Number." Você pode ler dela como Number. Você não pode adicionar nada a ela, novamente com a exceção de null — porque o compilador não sabe qual subtipo de Number a lista realmente contém. Adicionar um Integer a uma List<? extends Number> que é secretamente uma List<Double> a corromperia; em vez de tentar descobrir qual subtipo é, o compilador simplesmente recusa todo add.
Wildcard com limite inferior: ? super T
A imagem espelhada. ? super T significa "o tipo de elemento da lista é T ou algum supertipo de T":
public static void addOneTwoThree(List<? super Integer> list) {
list.add(1);
list.add(2);
list.add(3);
}
List<Integer> ints = new ArrayList<>(); addOneTwoThree(ints); // ✓
List<Number> nums = new ArrayList<>(); addOneTwoThree(nums); // ✓ — Number is a supertype of Integer
List<Object> objs = new ArrayList<>(); addOneTwoThree(objs); // ✓ — Object is tooAqui você pode adicionar qualquer Integer (ou subtipo) com segurança — o tipo de elemento da lista é garantidamente Integer ou algum ancestral dele, então um Integer cabe. O que você não pode fazer é ler um tipo específico — o máximo que pode dizer sobre um elemento é que é um Object, porque a lista real pode ser List<Object>.
A regra PECS
Existe um mnemônico que todo desenvolvedor Java eventualmente memoriza:
PECS — Producer Extends, Consumer Super.
É a regra prática para saber qual wildcard usar:
- Se o parâmetro produz valores (você lê dele): use
? extends T. - Se o parâmetro consome valores (você escreve nele): use
? super T.
A assinatura canônica que ela produz é Collections.copy:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}src é lido (ele produz Ts) — ? extends T. dest é escrito (ele consome Ts) — ? super T. Essa é toda a razão da assimetria: a mesma assinatura funciona se src for uma List<Integer> e dest for uma List<Number>, ou o contrário, desde que T seja um ponto de encontro entre eles.
Se você não se lembrar de mais nada deste capítulo, lembre-se de PECS.
Quando não usar um wildcard
Se um parâmetro é tanto lido quanto escrito no mesmo método, nem ? extends T nem ? super T funciona — nenhum permite fazer os dois. Nesse caso, use simplesmente um parâmetro de tipo normal:
public static <T> void swap(List<T> list, int i, int j) {
T tmp = list.get(i); // read
list.set(i, list.get(j)); // write
list.set(j, tmp); // write
}Um wildcard é a ferramenta certa quando um lado da relação é "só lejo" ou "só escrevo." Um parâmetro de tipo é a ferramenta certa quando você precisa se referir a um tipo de elemento específico nos dois lados.
Wildcards vs. parâmetros de tipo com limite
Compare:
public static <T extends Number> double sumNamed(List<T> list) { ... }
public static double sumWildcard(List<? extends Number> list) { ... }Funcionalmente, ambos aceitam o mesmo conjunto de argumentos. A diferença é o que o corpo pode expressar:
- A forma nomeada (
<T extends Number>) fornece um nomeT— útil se você quiser retornarT, aceitar outraList<T>como segundo parâmetro, ou escreverT tmp = list.get(0)para preservar o tipo de elemento exato. - A forma com wildcard (
? extends Number) não fornece nome — você só pode se referir aos elementos comoNumber. É mais restrita na API (nenhum nome vaza para a assinatura), mas menos expressiva no corpo.
Regra prática: se você só precisa dos elementos como Number, o wildcard é a escolha menor e mais limpa. Se o corpo precisa se referir a um T específico, nomeie-o.
Um exemplo trabalhado: PECS na prática
O programa copia elementos de uma lista para outra e calcula a soma acumulada — ambas as operações parametrizadas da forma PECS. Observe os call sites: copyOf(intList, numberList) mistura tipos de elementos porque os wildcards permitem que um destino Number aceite valores Integer.
sum aceita uma List<Integer> e uma List<Double> porque o wildcard diz "algum subtipo de Number." fillWithSquares adiciona valores Integer em uma List<Number> porque o wildcard diz "qualquer lista que possa conter Integer ou um de seus ancestrais." copyTo usa ambos — a origem é um produtor, o destino é um consumidor, e T é o tipo de elemento compartilhado que o compilador infere a partir do acordo entre os dois lados.
O que vem a seguir
Você viu as quatro formas como os generics aparecem no código-fonte — classes, métodos, interfaces e wildcards. Agora olhamos uma camada abaixo para ver como a JVM realmente implementa tudo isso. A resposta — type erasure — explica algumas restrições surpreendentes (sem new T(), sem instanceof T, sem arrays genéricos) e é o único insight que faz a história dos generics em Java fazer sentido. Continue para Java Type Erasure.