Generics in Java: Wildcards ? extends and ? super

1. Overview

Generics help make Java code type-safe at compile time. But when you write a method that takes List<Product>, you’ll find it won’t accept List<Book>, even though Book extends Product. This article explains why that’s the case. Then it guides you through how to use wildcards ? extends and ? super to write more flexible APIs. By the end, you’ll know how to choose the right wildcard using the PECS principle and avoid confusing compile errors.

2. The Problem: Generics Are Invariant

Let’s use a simple class hierarchy for this entire article:

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 is a Product. So is List<Book> a List<Product>?

List<Book> books = new ArrayList<>();
List<Product> products = books; // Compile error: incompatible types

The answer is no. Generics in Java are invariant. Even though A extends B, List<A> and List<B> are still two unrelated types.

The reason lies in type safety. Suppose the line above compiled, you could write:

products.add(new Product("Ballpoint pen", 5_000)); // adding a Product to a list that supposedly contains only Books
Book first = books.get(0);                          // ClassCastException!

Arrays are different. Arrays in Java are covariant, so similar errors only show up at runtime:

Object[] items = new String[2];
items[0] = 42; // compiles OK, throws ArrayStoreException at runtime

Generics choose to prevent errors at compile time. The cost is reduced flexibility. Wildcards were introduced to regain that flexibility.

3. Upper Bounded Wildcard: ? extends T

3.1. Reading Data from a Collection

Suppose you need to calculate the total price of a list of products. An initial version might look like this:

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

This method won’t accept List<Book>. We fix it with ? 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> means “a list containing elements of some type that is Product or a subclass of Product”. Thanks to this:

  • You can pass List<Product>, List<Book>, or List<Ebook> if it exists.
  • Each element read out is guaranteed to be a Product, so the for (Product p : products) loop is valid.
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. Why Can’t You Add Elements?

Try adding an element to a list with ? extends:

static void addBook(List<? extends Product> products) {
    products.add(new Book("Refactoring", 400_000)); // Compile error
    products.add(null);                              // OK, but pointless
}

The compiler doesn’t know what ? really is. The passed-in list might be List<Ebook>. Adding a Book there would break type safety. So the compiler forbids any add operation, except for null.

Easy rule to remember: ? extends T is for reading T only. The collection acts as a producer, meaning a source of data for you.

4. Lower Bounded Wildcard: ? super T

Now let’s do the opposite. You want a method that loads default books into any list that can hold 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> means “a list whose element type is Book or a superclass of Book”. Any such type can hold a Book, so add(new Book(...)) is always type-safe.

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

In exchange, when reading, you only know for sure the element is Object:

static void printFirst(List<? super Book> target) {
    Object first = target.get(0); // OK
    Book book = target.get(0);    // Compile error
}

The list might be List<Object> containing anything, so the compiler can only guarantee type Object.

Rule: ? super T is for writing T in. The collection acts as a consumer, meaning a receiver of data you provide.

5. The PECS Principle

The two rules above combine into PECS: Producer Extends, Consumer Super.

  • Use ? extends T for a parameter that supplies T to you.
  • Use ? super T for a parameter that accepts T from you.
  • For a parameter that both reads and writes, use the exact type T without wildcards.

5.1. Example: Copying Between Two Lists

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

source is a producer so use extends. target is a consumer so use super. The result is a very flexible method:

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

CatalogUtils.<Book>copy(books, cart);   // T = Book: reads Books, writes to superclass list
CatalogUtils.<Product>copy(books, cart); // T = Product is also valid

The JDK follows this principle correctly. The signature of Collections.copy is:

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

5.2. Example: Comparator for Superclass

List.sort has the signature sort(Comparator<? super E> c). Comparator is a consumer because it takes elements to compare. Thanks to ? super, you can use a comparator written for Product to sort a 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

You’ll see this pattern frequently in the Stream API. For example, map(Function<? super T, ? extends R> mapper): the input of the function is a consumer (super), the output is a producer (extends).

6. Unbounded Wildcard: ?

When a method doesn’t care about the element type, use ?:

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

List<?> accepts any list: List<Book>, List<String>, List<Integer>. Elements are read as Object, and you can’t add anything except null.

Don’t confuse List<?> with List<Object>. A method taking List<Object> will not accept List<String>, also due to invariance from section 2.

7. Testing the Behavior

Add JUnit 5 and AssertJ to 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>

If your project uses Spring Boot 3.x, spring-boot-starter-test already includes both libraries.

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("not a book");

        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);
    }
}

The first three tests confirm the methods accept subtypes and supertypes as expected. The second test shows that List<Object> retains its original element and accepts additional Books. The last test reminds us of something important: wildcards only check type at compile time. It doesn’t know whether the list allows modification.

8. Common Mistakes and When Not to Use Wildcards

Using wildcards in return types.

public static List<? extends Product> findAll() { ... } // not recommended

The caller will receive a list that’s almost read-only, and will have to carry ? everywhere. Wildcards should be in the parameters of methods, not in return types.

Using ? extends and trying to call add. If the method needs to write to the collection, extends is the wrong choice. Reconsider PECS.

Using wildcards when you need both reading and writing with exact types. For example, swapping two elements:

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);
}

Calling list.set(i, list.get(j)) directly on List<?> will be a compile error. The generic helper <T> “captures” the actual type of ? to enable the operations. This technique is called wildcard capture. If your public API doesn’t require List<?>, declaring <T> void swap(List<T> list, ...) directly is simpler.

Overusing wildcards for internal classes. Wildcards are most useful in shared APIs like libraries or utilities. For private methods called from one place, explicit types are usually more readable.

Note: ? extends doesn’t make the list immutable. You can still call clear() or remove(...), because these operations don’t need to know the element type. If you need to protect the data, use List.copyOf(...) or Collections.unmodifiableList(...).

Note: You can’t use both extends and super on the same wildcard. You also can’t write ? super for a type parameter declared on a class or method (<T super Book> is invalid).

9. Conclusion

Generics in Java are invariant. So List<Book> is not List<Product>, and this helps prevent type errors at runtime. Wildcards restore flexibility to your API:

  • Use ? extends T for parameters that supply data. You can read T but can’t add.
  • Use ? super T for parameters that accept data. You can add T but can only read Object.
  • Use ? when you don’t care about the element type.

When in doubt, just remember PECS: Producer Extends, Consumer Super. Also, keep wildcards in method parameters, not in return types.

javagenericswildcardpecscollections