Upper vs Lower Bounds in Java Generics: When to Use extends vs super

Illustration for Upper vs Lower Bounds in Java Generics: When to Use extends vs super
By Last updated:

Generics in Java provide type safety, reusability, and maintainability. A key part of generics is bounded wildcards using extends and super. These allow developers to restrict what types can be used while still maintaining flexibility in APIs.

The common confusion is knowing when to use ? extends T (upper bound) vs ? super T (lower bound). This is where the PECS principle (Producer Extends, Consumer Super) comes in.

Think of it this way:

  • extends is like a vending machine (you can take items out, but not put new ones in).
  • super is like a donation box (you can put items in, but can’t guarantee what you’ll get out).

This tutorial explores upper and lower bounds, explains their differences, and demonstrates real-world applications.


Core Definition and Purpose of Java Generics

Generics allow code to be parameterized with types, leading to safer, cleaner, and reusable APIs. Bounds (extends, super) refine these generic parameters by restricting or broadening the type hierarchy.


Introduction to Type Parameters: <T>, <E>, <K, V>

  • <T> – General type parameter.
  • <E> – Element type (used in collections).
  • <K, V> – Key-Value pairs (used in maps).

Upper Bounded Wildcards: ? extends T

Upper bounds restrict a wildcard to a type or its subtypes.

public static void printList(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
}

Usage:

List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
printList(ints);
printList(doubles);
  • Safe for reading (Producer Extends).
  • Cannot add new elements except null.

Lower Bounded Wildcards: ? super T

Lower bounds restrict a wildcard to a type or its supertypes.

public static void addNumbers(List<? super Integer> list) {
    list.add(42);
    list.add(100);
}

Usage:

List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
addNumbers(ints);
addNumbers(nums);
  • Safe for writing (Consumer Super).
  • Reading returns Object, since type is unknown.

PECS Principle: Producer Extends, Consumer Super

  • Use extends when your method produces data (you only read).
  • Use super when your method consumes data (you write).

Example:

public static void copy(List<? extends Number> src, List<? super Number> dest) {
    for (Number n : src) dest.add(n);
}

Multiple Type Parameters with Bounds

public static <K extends Comparable<K>, V extends Number> void process(Map<K, V> map) {
    for (K key : map.keySet()) {
        System.out.println("Key: " + key);
    }
}

Wildcards vs Type Parameters

  • Type parameters (<T extends Number>) are used when designing APIs.
  • Wildcards (? extends Number) are used when consuming existing APIs.

Generics in Collections Framework

Example with List and extends

List<? extends Number> numbers = Arrays.asList(1, 2.5, 3);
Number n = numbers.get(0); // Safe
// numbers.add(10); // Not allowed

Example with List and super

List<? super Integer> integers = new ArrayList<Number>();
integers.add(5); // Safe
Object obj = integers.get(0); // Returns Object

Raw Types vs Parameterized Types

List rawList = new ArrayList(); // Unsafe
List<? extends Number> safeList = new ArrayList<Integer>();

Type Inference and Diamond Operator

Map<String, ? extends Number> map = new HashMap<>();

Type Erasure and Bounded Wildcards

At runtime, both ? extends Number and ? super Integer are erased to List. Type checks exist only at compile time.


Reifiable vs Non-Reifiable Types

  • Reifiable: List<?>.
  • Non-Reifiable: List<? extends Number>.

Recursive Type Bounds

public static <T extends Comparable<T>> T max(List<T> list) {
    return list.stream().max(Comparator.naturalOrder()).orElse(null);
}

Designing Fluent APIs with Bounds

class Builder<T extends Builder<T>> {
    public T withName(String name) { return (T) this; }
}

Generics with Enums and Annotations

EnumSet<? extends Enum<?>> days = EnumSet.allOf(Day.class);

Case Studies

Type-Safe Cache

class Cache<K, V> {
    private Map<K, V> store = new HashMap<>();
    public void put(K key, V value) { store.put(key, value); }
    public V get(K key) { return store.get(key); }
}

Flexible Repository Pattern

interface Repository<T, ID> {
    void save(T entity);
    T findById(ID id);
}

Event Handling System

interface EventListener<E> {
    void onEvent(List<? extends E> events);
}

Copy Utility with PECS

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

Best Practices for Using extends and super

  • Use extends when a method only reads.
  • Use super when a method only writes.
  • Prefer wildcards in API design.
  • Avoid mixing raw types with bounded generics.

Common Anti-Patterns

  • Overusing wildcards where type parameters suffice.
  • Deeply nested bounds (<? extends List<? super T>>).
  • Ignoring the PECS principle.

Performance Considerations

Bounds (extends, super) impose no runtime cost. Checks are enforced at compile time, and erased at runtime.


📌 What's New in Java for Generics?

  • Java 5: Generics introduced with extends and super.
  • Java 7: Diamond operator simplified generic instantiation.
  • Java 8: Streams and lambdas integrated bounds extensively.
  • Java 10: var works with generics.
  • Java 17+: Sealed classes fit naturally with bounded generics.
  • Java 21: Virtual threads enhance concurrent collections using bounds.

Conclusion and Key Takeaways

Understanding when to use extends vs super is crucial for designing flexible, type-safe APIs. Remember the PECS principle:

  • Producer Extends → Use extends for reading.
  • Consumer Super → Use super for writing.

By mastering bounded wildcards, developers can avoid ClassCastException, simplify code, and design scalable APIs.


FAQ on Upper vs Lower Bounds

Q1: Why can’t I add elements to a List<? extends T>?
Because the compiler cannot guarantee the subtype at runtime.

Q2: Can I always use ? extends Object instead of ? super T?
No, super ensures safe writing, extends ensures safe reading.

Q3: What’s the difference between <T> and ? extends T?
<T> is for defining generic classes/methods; ? extends T is for consuming them.

Q4: Why does ? super T return Object when reading?
Because the lower bound only guarantees it’s some supertype of T.

Q5: Can I mix extends and super in one method?
Yes, often used in copy utilities (copy(List<? extends T>, List<? super T>)).

Q6: Does PECS apply only to collections?
No, it applies anywhere wildcards define input/output behavior.

Q7: How does type erasure affect bounds?
Both ? extends and ? super are erased to raw types at runtime.

Q8: Do bounds affect performance?
No, all checks happen at compile time.

Q9: Can I use wildcards with Maps?
Yes, e.g., Map<? extends Number, ? super String>.

Q10: Should I prefer wildcards or explicit type parameters?
Wildcards are better for API consumers, type parameters for API definitions.