Generics em Java: wildcard ? extends e ? super

1. Visão geral

Generics ajudam o código Java a ser seguro em relação ao tipo em tempo de compilação. Mas quando você escreve um method que recebe List<Product>, verá que ele não aceita List<Book>, mesmo que Book seja uma subclasse de Product. Este artigo explica por que isso acontece. Depois, orienta como usar wildcards ? extends e ? super para escrever APIs mais flexíveis. Ao final, você saberá escolher qual wildcard usar seguindo o princípio PECS e evitará erros de compilação confusos.

2. O problema: generics são invariantes

Usaremos uma hierarquia de classes simples em todo o artigo:

public class Product {
    private final String name;
    private final double price;

    public Product(String name, double price) {
        this.name = name;
        this.price = price;
    }

    public String name() { return name; }
    public double price() { return price; }
}

public class Book extends Product {
    public Book(String name, double price) {
        super(name, price);
    }
}

Book é um Product. Então, List<Book> é um List<Product>?

List<Book> books = new ArrayList<>();
List<Product> products = books; // Erro de compilação: tipos incompatíveis

A resposta é não. Generics em Java são invariantes. Com A sendo subclasse de B, List<A> e List<B> continuam sendo dois tipos não relacionados.

O motivo está na segurança de tipos em si. Suponha que a linha acima compilasse, poderíamos escrever:

products.add(new Product("Caneta", 5_000)); // adiciona Product em uma lista "contendo apenas Book"
Book first = books.get(0);                    // ClassCastException!

Arrays são diferentes. Arrays em Java são covariantes, então o erro similar só aparece em runtime:

Object[] items = new String[2];
items[0] = 42; // compila OK, em runtime lança ArrayStoreException

Generics escolhem bloquear o erro já em tempo de compilação. O preço a pagar é menos flexibilidade. Wildcards foram criados para recuperar essa flexibilidade.

3. Upper bounded wildcard: ? extends T

3.1. Lendo dados de uma collection

Suponha que você precise calcular o preço total de uma lista de produtos. Uma primeira versão geralmente fica assim:

static double totalPrice(List<Product> products) { ... }

Este method não aceita List<Book>. Corrigimos usando ? extends:

public final class CatalogUtils {

    public static double totalPrice(List<? extends Product> products) {
        double total = 0;
        for (Product p : products) {
            total += p.price();
        }
        return total;
    }
}

List<? extends Product> significa “uma lista contendo elementos de algum tipo que é Product ou subclasse de Product”. Graças a isso:

  • Você pode passar List<Product>, List<Book>, ou List<Ebook> se existir.
  • Cada elemento lido com certeza é um Product, então o loop for (Product p : products) é válido.
List<Book> books = List.of(
        new Book("Clean Code", 300_000),
        new Book("Effective Java", 450_000));

System.out.println(CatalogUtils.totalPrice(books));
750000.0

3.2. Por que não conseguir adicionar elemento?

Tente adicionar um elemento em uma lista com ? extends:

static void addBook(List<? extends Product> products) {
    products.add(new Book("Refactoring", 400_000)); // Erro de compilação
    products.add(null);                              // OK, mas sem sentido
}

O compilador não sabe o que ? realmente é. A lista passada pode ser List<Ebook>. Nesse caso, adicionar um Book assim quebraria a segurança de tipos. Por isso o compilador proíbe toda operação add, exceto null.

Regra fácil de lembrar: ? extends T é apenas para ler T. A collection nesse caso faz papel de produtor, ou seja, um fornecedor de dados para você.

4. Lower bounded wildcard: ? super T

Agora faça o inverso. Você quer um method que carregue livros padrão em qualquer lista que possa conter Book:

public static void addDefaultBooks(List<? super Book> target) {
    target.add(new Book("Clean Code", 300_000));
    target.add(new Book("Effective Java", 450_000));
}

List<? super Book> significa “uma lista cujo tipo de elemento é Book ou superclasse de Book”. Qualquer tipo assim consegue conter um Book, então add(new Book(...)) é sempre seguro.

List<Book> books = new ArrayList<>();
List<Product> products = new ArrayList<>();
List<Object> anything = new ArrayList<>();

CatalogUtils.addDefaultBooks(books);    // OK
CatalogUtils.addDefaultBooks(products); // OK
CatalogUtils.addDefaultBooks(anything); // OK

System.out.println(products.size());
2

Em contrapartida, ao ler, você só consegue garantir que o elemento é Object:

static void printFirst(List<? super Book> target) {
    Object first = target.get(0); // OK
    Book book = target.get(0);    // Erro de compilação
}

A lista pode ser List<Object> contendo qualquer coisa, então o compilador só consegue garantir tipo Object.

Regra: ? super T é para escrever T. A collection agora é consumidora, ou seja, um lugar que consome dados que você fornece.

5. Princípio PECS

As duas regras acima se combinam em PECS: Producer Extends, Consumer Super.

  • Um parâmetro que fornece T para você usa ? extends T.
  • Um parâmetro que recebe T de você usa ? super T.
  • Um parâmetro que lê e escreve simultaneamente não usa wildcard, declare o tipo exato T.

5.1. Exemplo: copiar entre duas listas

public static <T> void copy(List<? extends T> source, List<? super T> target) {
    for (T item : source) {
        target.add(item);
    }
}

source é produtor então usa extends. target é consumidor então usa super. O resultado é um method muito flexível:

List<Book> books = List.of(new Book("DDD", 500_000));
List<Product> cart = new ArrayList<>();

CatalogUtils.<Book>copy(books, cart);   // T = Book: lê Book, escreve em lista superclasse
CatalogUtils.<Product>copy(books, cart); // T = Product também é válido

A JDK aplica corretamente esse princípio. A assinatura de Collections.copy é:

public static <T> void copy(List<? super T> dest, List<? extends T> src)

5.2. Exemplo: Comparator reutilizado da superclasse

List.sort tem assinatura sort(Comparator<? super E> c). Comparator é consumidor, pois recebe elementos para comparação. Graças a ? super, você pode usar um comparator escrito para Product a fim de ordenar List<Book>:

Comparator<Product> byPrice = Comparator.comparingDouble(Product::price);

List<Book> books = new ArrayList<>(List.of(
        new Book("Effective Java", 450_000),
        new Book("Clean Code", 300_000)));

books.sort(byPrice);
books.forEach(b -> System.out.println(b.name()));
Clean Code
Effective Java

Você encontrará esse padrão muito na Stream API. Por exemplo map(Function<? super T, ? extends R> mapper): input da função é consumidor (super), output é produtor (extends).

6. Unbounded wildcard: ?

Quando o method não se importa com o tipo de elemento, use ?:

public static int countNonNull(List<?> items) {
    int count = 0;
    for (Object item : items) {
        if (item != null) count++;
    }
    return count;
}

List<?> aceita qualquer lista: List<Book>, List<String>, List<Integer>. Elementos lidos são Object, e você não consegue adicionar nada além de null.

Não confunda List<?> com List<Object>. Um method que recebe List<Object> não recebe List<String>, também pela invariância mostrada na seção 2.

7. Testando o comportamento

Adicione JUnit 5 e AssertJ ao pom.xml:

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.11.3</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.26.3</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Se seu projeto usa Spring Boot 3.x, spring-boot-starter-test já inclui ambas as bibliotecas.

import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CatalogUtilsTest {

    @Test
    void totalPrice_acceptsListOfSubtype() {
        List<Book> books = List.of(
                new Book("Clean Code", 300_000),
                new Book("Effective Java", 450_000));

        assertThat(CatalogUtils.totalPrice(books)).isEqualTo(750_000.0);
    }

    @Test
    void addDefaultBooks_worksWithSupertypeList() {
        List<Object> anything = new ArrayList<>();
        anything.add("não é um livro");

        CatalogUtils.addDefaultBooks(anything);

        assertThat(anything).hasSize(3);
        assertThat(anything.get(1)).isInstanceOf(Book.class);
    }

    @Test
    void copy_movesBooksIntoProductList() {
        List<Book> books = List.of(new Book("DDD", 500_000));
        List<Product> cart = new ArrayList<>();

        CatalogUtils.copy(books, cart);

        assertThat(cart).extracting(Product::name).containsExactly("DDD");
    }

    @Test
    void addDefaultBooks_failsAtRuntimeOnImmutableList() {
        List<Product> immutable = List.of();

        assertThatThrownBy(() -> CatalogUtils.addDefaultBooks(immutable))
                .isInstanceOf(UnsupportedOperationException.class);
    }
}

Os três primeiros testes confirmam que os methods aceitam subtipos e supertipos como esperado. O segundo teste mostra que List<Object> mantém elementos antigos e recebe Book novo. O último lembra algo importante: wildcard só verifica tipo em compilação. Ele não sabe se a lista permite modificação.

8. Erros comuns e quando não usar

Usar wildcard no tipo retornado.

public static List<? extends Product> findAll() { ... } // não faça

O chamador receberá uma lista praticamente somente-leitura, e terá que carregar ? por toda parte. Wildcard deve ficar nos parâmetros do method, não no tipo retornado.

Usar ? extends e depois tentar chamar add. Se o method precisa escrever na collection, extends é escolha errada. Reveja PECS.

Usar wildcard quando precisa ler e escrever do tipo exato. Exemplo, trocar dois elementos:

public static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

private static <T> void swapHelper(List<T> list, int i, int j) {
    T tmp = list.get(i);
    list.set(i, list.get(j));
    list.set(j, tmp);
}

Chamar list.set(i, list.get(j)) direto em List<?> dará erro de compilação. O helper genérico <T> “captura” o tipo real do ? para conseguir operar. Essa técnica chama-se wildcard capture. Se a API pública não obrigar List<?>, declarar direto <T> void swap(List<T> list, ...) é mais simples.

Abuser de wildcard em classes internas. Wildcard é mais útil em APIs reutilizáveis como bibliotecas ou utilitários. Para methods privados chamados de um único lugar, tipo específico geralmente é mais legível.

Nota: ? extends não torna a lista imutável. O method ainda consegue chamar clear() ou remove(...), já que essas operações não precisam saber o tipo do elemento. Se precisar proteger dados, use List.copyOf(...) ou Collections.unmodifiableList(...).

Nota: Não dá para usar extends e super no mesmo wildcard. Também não dá escrever ? super em declaração de type parameter de classe ou method (<T super Book> é inválido).

9. Conclusão

Generics em Java são invariantes. Por isso List<Book> não é List<Product>, e isso evita erros de tipo em runtime. Wildcards recuperam flexibilidade na API:

  • ? extends T usa-se em parâmetros que fornecem dados. Você lê T mas não consegue adicionar.
  • ? super T usa-se em parâmetros que recebem dados. Você adiciona T mas só lê Object.
  • ? usa-se quando não se importa com o tipo do elemento.

Quando em dúvida, basta lembrar PECS: Producer Extends, Consumer Super. Além disso, mantenha wildcard nos parâmetros do method, não no tipo retornado.

javagenericswildcardpecscollections