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>, ouList<Ebook>se existir. - Cada elemento lido com certeza é um
Product, então o loopfor (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
Tpara você usa? extends T. - Um parâmetro que recebe
Tde 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:
? extendsnão torna a lista imutável. O method ainda consegue chamarclear()ouremove(...), já que essas operações não precisam saber o tipo do elemento. Se precisar proteger dados, useList.copyOf(...)ouCollections.unmodifiableList(...).
Nota: Não dá para usar
extendsesuperno mesmo wildcard. Também não dá escrever? superem 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 Tusa-se em parâmetros que fornecem dados. Você lêTmas não consegue adicionar.? super Tusa-se em parâmetros que recebem dados. Você adicionaTmas 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.