Atualizado: 02/11/2025

Este conteúdo é original e não foi gerado por inteligência artificial.

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:

  1. Wildcard não limitado: ?
  2. Wildcard limitado superiormente: ? extends Tipo
  3. 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 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 extends ao ler e super ao 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 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.