Upper / Lower Bound Generics in Java!

The “Invariance” Problem

Imagine you have a family hierarchy: Number is the parent, and Integer and Double are its children.


Naturally, because of polymorphism, you can do this:

Number num = Integer.valueOf(5); // Totally legal!

But what happens when we put them in a list?

List<Number> numberList = new ArrayList<Integer>(); // Compile error!

This fails because Java generics are invariant. Java does this to protect you.

If it allowed the code above, you could subsequently run numberList.add(3.14) (a Double), which would corrupt the underlying ArrayList<Integer> with a double!

So, List<Integer> is not a subclass of List<Number>.

BUT what if you write a method that only reads values and does not add anything? Say, a method to sum numbers:

public static double sumOfList(List<Number> list) { ... }

Because of invariance, you can only pass a List<Number>. You cannot pass a List<Integer> or List<Double>, even though summing them is perfectly safe.

This is where Upper-Bound Generics (? extends T) save the day.

Upper-Bound Wildcards (? extends T)

By changing the method signature to use an upper-bound wildcard, you are telling Java: “This method accepts a List containing elements of type T or any subclass of T.”

= “allowing a container of a subtype to be assigned to a container of a supertype”

= “Covariant”

public static double sumOfList(List<? extends Number> list) {
    double sum = 0.0;
    for (Number n : list) {
        sum += n.doubleValue(); // Safe because we know whatever is in the list IS a Number
    }
    return sum;
}

Now, you can safely pass a List<Integer>, List<Double>, or List<Float> to sumOfList().

The Golden Rule: Read-Only (Producer Extends)

When you use ? extends Number, Java guarantees that whatever comes out of that list can be safely treated as a Number.

However, you cannot add anything to this list (except null).

List<? extends Number> list = new ArrayList<Integer>();
list.add(5); // COMPILE ERROR!

Why? Because the compiler doesn’t know if list is actually pointing to a ArrayList<Double> or an ArrayList<Float>. To prevent you from putting an Integer into a Double list, it blocks all write operations.

Intuition: extends = Read-Only. Use it when your generic structure is producing data for you to read.

The Destination Problem

Let’s stick with our family hierarchy: Object \rightarrow Number \rightarrow Integer.

Imagine you are writing a method that generates random whole numbers and saves them into a list. Naturally, you might start with a signature like this:

public static void addIntegers(List<Integer> list) {
    list.add(42); 
}

This works, but it’s rigidly restrictive. Is it safe to add an Integer to a List<Number>? Yes, because an Integer is a Number. Is it safe to add an Integer to a List<Object>? Absolutely, because everything is an Object.

But because Java generics are invariant, you cannot pass a List<Number> or a List<Object> into that method. You are forced to pass only a strict List<Integer>.

This is where Lower-Bound Generics save the day.

Lower-Bound Wildcards (? super T)

If upper bounds are all about safely reading data, lower bounds are all about safely writing data.

By changing the method signature to use a lower-bound wildcard, you tell Java: “This method accepts a List of type T or any superclass (parent) of T.”

= a relationship where two things change in opposite directions (inheritance direction) = “Contravariant”

public static void addIntegers(List<? super Integer> list) {
    list.add(42); // Perfectly safe!
}

Now, you can comfortably pass a List<Integer>, a List<Number>, or a List<Object> to this method.

The Golden Rule: Write-Only (Consumer Super)

When you use ? super Integer, Java guarantees that whatever the list’s actual type is, it is a parent type of Integer. Because of this, it is 100% safe to add an Integer (or its subclasses) to that list.

However, reading data from this list is heavily restricted.

List<? super Integer> list = new ArrayList<Number>();
// What comes out? Java doesn't know if it's a Number list or an Object list.
Integer item = list.get(0); // COMPILE ERROR!
Object obj = list.get(0);    // Allowed, because everything is an Object.

Why? Because the compiler can’t guarantee if the underlying list is a List<Number> or a List<Object>. The only thing it knows for sure is that whatever comes out is at least an Object.

Intuition: super = Write-Oriented. Use it when your generic structure is consuming data (storing things you provide).

Summary: The PECS Mnemonic

To tie both concepts together perfectly, Java developers use the PECS rule coined by Joshua Bloch:

Producer Extends, Consumer Super.

  • Producer (? extends T): If you only read data from a collection, it is a producer. Use extends.
  • Consumer (? super T): If you only write data into a collection, it is a consumer. Use super.
  • Both: If you need to both read from and write to the same collection, do not use wildcards at all (just use List<T>).

Real-World Java API Example

Collections.copy(List<? super T> dest, List<? extends T> src)

The classic copy utility showcases both bounds:

// Inside java.util.Collections
public static <T> void copy(List<? super T> dest, List<? extends T> src)
  • src is the producer (we only read from it) \rightarrow ? extends T (Upper Bound)
  • dest is the consumer (we write to it) \rightarrow ? super T (Lower Bound)

Leave a Reply

Your email address will not be published. Required fields are marked *