Wildcards em Java: Uso de Tipos Coringa em Genéricos
Os genéricos em Java têm papel essencial na segurança de tipos e na flexibilidade do código. Entretanto, quando combinados com herança, podem gerar situações complexas. É nesse ponto que entram os wildcards — também chamados de tipos coringa — um recurso poderoso que permite trabalhar com tipos desconhecidos de forma segura e genérica.
Um wildcard representa um tipo desconhecido e é indicado por um ponto de interrogação (?).
Ele torna possível criar métodos e classes que funcionam com diferentes tipos de dados, mantendo a verificação de tipos em tempo de compilação.
Existem três formas principais de wildcards:
- Wildcard não limitado:
? - Wildcard limitado superiormente:
? extends Tipo - Wildcard limitado inferiormente:
? super Tipo
A seguir, cada uma dessas variações é explicada em detalhes com exemplos práticos.
Classe base usada nos exemplos
class Person<T> {
private T id;
private String name;
T getId() { return id; }
void setId(T id) { this.id = id; }
String getName() { return name; }
Person(T id, String name) {
this.id = id;
this.name = name;
}
}A classe Person é genérica e parametrizada por T, que define o tipo do campo id.
Por que precisamos de wildcards
Considere o código abaixo:
public class Program {
public static void main(String[] args) {
Person<Object> bob = new Person<>(2.0, "Bob");
Person<Integer> tom = new Person<>(1, "Tom");
Person<String> sam = new Person<>("F-456", "Sam");
printPersonInfo(bob);
printPersonInfo(tom); // Erro de compilação
printPersonInfo(sam); // Erro de compilação
}
static void printPersonInfo(Person<Object> person) {
System.out.print("Name: " + person.getName());
Object id = person.getId();
System.out.println("; Id: " + id);
}
}À primeira vista, parece que Person<Object> poderia aceitar qualquer tipo, já que Object é a classe base de todas as outras.
No entanto, Person<Integer> não é um subtipo de Person<Object> — a tipagem genérica é invariante em Java.
Ou seja, List<Integer> não é um subtipo de List<Object>, e o mesmo vale para Person<Integer> e Person<Object>.
Os wildcards resolvem exatamente esse tipo de problema.
Wildcard não limitado (?)
O wildcard não limitado é usado quando o tipo genérico é irrelevante para a lógica do método. Ele representa “qualquer tipo”, sem impor restrições.
public class Program {
public static void main(String[] args) {
Person<Integer> tom = new Person<>(123, "Tom");
Person<String> bob = new Person<>("A-456", "Bob");
printPersonInfo(tom); // Name: Tom; Id: 123
System.out.println();
printPersonInfo(bob); // Name: Bob; Id: A-456
}
static void printPersonInfo(Person<?> person) {
System.out.print("Name: " + person.getName());
// Podemos obter o ID, mas o compilador o tratará como Object
Object id = person.getId();
System.out.println("; Id: " + id);
}
}O parâmetro Person<?> indica que o método aceita qualquer tipo de Person, independentemente de T.
Isso é útil quando apenas lemos os dados, sem precisar conhecer o tipo exato de id.
Porém, como o compilador não sabe qual é o tipo real de T, não é permitido alterar o valor do campo genérico:
static void changePerson(Person<?> person, Object id) {
person.setId(id); // Erro de compilação
}Essa restrição impede a quebra da segurança de tipos em tempo de execução.
Wildcard limitado superiormente (? extends Tipo)
O wildcard limitado superiormente (? extends Tipo) indica que o tipo é Tipo ou um de seus subtipos.
A palavra “superior” vem da posição de Tipo no topo da hierarquia de herança — as subclasses estão “abaixo” dele.
Esse tipo de wildcard é usado quando o método lê valores de uma estrutura genérica.
Exemplo:
public class Program {
public static void main(String[] args) {
Person<Integer> tom = new Person<>(10, "Tom");
Person<Double> bob = new Person<>(25.5, "Bob");
processNumericId(tom); // Id as double: 10.0
processNumericId(bob); // Id as double: 25.5
}
static void processNumericId(Person<? extends Number> person) {
Number id = person.getId(); // leitura segura
System.out.println("Id as double: " + id.doubleValue());
}
}Aqui, o método aceita qualquer Person cujo tipo T seja Number ou uma subclasse, como Integer ou Double.
O campo id pode ser lido como Number, e seus métodos, como doubleValue(), podem ser usados.
Mas observe: não é possível definir um novo valor no campo id:
static void setNumericId(Person<? extends Number> person, Number id) {
person.setId(id); // Erro: o compilador não sabe se o tipo é Integer ou Double
}Wildcard limitado inferiormente (? super Tipo)
O wildcard limitado inferiormente (? super Tipo) indica que o tipo é Tipo ou algum de seus supertipos.
Nesse caso, a hierarquia é “descendente”: qualquer classe acima de Tipo na árvore de herança é aceita.
Esse wildcard é útil quando o método escreve valores em uma estrutura genérica.
Exemplo:
public class Program {
public static void main(String[] args) {
Person<Integer> pInt = new Person<>(1, "Tom");
Person<Number> pNum = new Person<>(2.0, "Bob");
Person<Object> pObj = new Person<>("F-456", "Sam");
setIntegerId(pInt, 100);
setIntegerId(pNum, 200);
setIntegerId(pObj, 300);
System.out.println("pInt Id: " + pInt.getId());
System.out.println("pNum Id: " + pNum.getId());
System.out.println("pObj Id: " + pObj.getId());
}
static void setIntegerId(Person<? super Integer> person, int newId) {
person.setId(newId); // gravação segura
}
}Como Integer é um subtipo de Number e Object, qualquer Person parametrizado com esses tipos pode receber um Integer com segurança.
Entretanto, ao ler o valor, o compilador só garante que o retorno é Object, pois não sabe o tipo exato:
static void printPersonId(Person<? super Integer> person) {
Object id = person.getId();
System.out.println(id);
}Princípio PECS
O princípio PECS (Producer Extends, Consumer Super) ajuda a lembrar qual wildcard usar:
- Producer Extends: se o objeto fornece dados (operações de leitura), use
? extends Tipo. - Consumer Super: se o objeto consome dados (operações de escrita), use
? super Tipo.
Ou, em resumo:
Use
extendsao ler esuperao escrever.
Resumo
- Wildcards permitem manipular genéricos sem conhecer o tipo exato.
?representa qualquer tipo — útil quando a lógica não depende do tipo genérico.? extends Tipoé usado quando o método lê valores genéricos.? super Tipoé usado quando o método escreve valores genéricos.- O compilador bloqueia operações que possam comprometer a segurança de tipos.
- O princípio PECS resume o uso correto: Producer Extends, Consumer Super.