Skip to content

The super Wildcard

We previously discussed the inheritance relationship of generics: Pair<Integer> is not a subclass of Pair<Number>.

Let's examine the following set method:

java
void set(Pair<Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

Passing Pair<Integer> is allowed, but passing Pair<Number> is not.

In contrast to the extends wildcard, this time we want to accept Pair<Integer> types, as well as Pair<Number> and Pair<Object>, because Number and Object are superclasses of Integer, and setFirst(Number) and setFirst(Object) effectively allow accepting Integer types.

We use the super wildcard to rewrite this method:

java
void set(Pair<? super Integer> p, Integer first, Integer last) {
    p.setFirst(first);
    p.setLast(last);
}

Note that Pair<? super Integer> means the method parameter accepts any Pair type whose generic type is Integer or a superclass of Integer.

The following code can be compiled without errors:

java
public class Main {
    public static void main(String[] args) {
        Pair<Number> p1 = new Pair<>(12.3, 4.56);
        Pair<Integer> p2 = new Pair<>(123, 456);
        setSame(p1, 100);
        setSame(p2, 200);
        System.out.println(p1.getFirst() + ", " + p1.getLast());
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }

    static void setSame(Pair<? super Integer> p, Integer n) {
        p.setFirst(n);
        p.setLast(n);
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

Examining the setFirst() method of Pair<? super Integer>, its method signature is effectively:

java
void setFirst(? super Integer);

Thus, it is safe to pass an Integer type.

Now, let's look at the getFirst() method of Pair<? super Integer>, whose method signature is:

java
? super Integer getFirst();

Here, we note that we cannot use an Integer type to receive the return value of getFirst(), meaning the following statement will not compile:

java
Integer x = p.getFirst();

This is because if the actual type passed is Pair<Number>, the compiler cannot cast Number to Integer.

Note: Although Number is an abstract class and cannot be instantiated directly, even if Number were not abstract, this would still not compile. Additionally, when passing a Pair<Object> type, the compiler cannot cast Object to Integer.

The only type that can receive the return value of getFirst() is Object:

java
Object obj = p.getFirst();

Therefore, using <? super Integer> indicates:

  • It allows calling the set(? super Integer) method to pass in a reference of Integer.
  • It does not allow calling the get() method to obtain a reference of Integer, with the only exception being retrieving an Object reference: Object o = p.getFirst().

In other words, using <? super Integer> as a method parameter means the method's internal code can only write to the parameter, but cannot read from it.

Comparing extends and super Wildcards

Let’s revisit the extends wildcard. The differences between <? extends T> and <? super T> as method parameters are:

  • <? extends T> allows calling the read method T get() to obtain a reference of T, but does not allow calling the write method set(T) to pass in a reference of T (except for null).
  • <? super T> allows calling the write method set(T) to pass in a reference of T, but does not allow calling the read method T get() to obtain a reference of T (except for obtaining an Object).

One allows reading but not writing, while the other allows writing but not reading.

Keep the above conclusions in mind as we look at the copy() method defined in the Java standard library's Collections class:

java
public class Collections {
    // Copies each element from src to dest:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i = 0; i < src.size(); i++) {
            T t = src.get(i);
            dest.add(t);
        }
    }
}

This method copies each element from one List to another. The first parameter is List<? super T>, representing the destination list, while the second parameter is List<? extends T>, representing the source list. We can simply implement the copy using a for loop. In the loop, we see that for the variable of type <? extends T>, src, we can safely obtain a reference of type T, while for the variable of type <? super T>, dest, we can safely pass in a reference of T.

The definition of this copy() method perfectly illustrates the intent of extends and super:

  • The copy() method does not read from dest, since it cannot call dest.get() to obtain a reference of T.
  • The copy() method does not modify src, since it cannot call src.add(T).

This is enforced by the compiler. If the method code unexpectedly modifies src or unexpectedly reads from dest, it will result in a compilation error:

java
public class Collections {
    // Copies each element from src to dest:
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        ...
        T t = dest.get(0); // compile error!
        src.add(t); // compile error!
    }
}

Another benefit of this copy() method is that it allows safely adding a List<Integer> to a List<Number>, but not the other way around:

java
// Copy List<Integer> to List<Number> - ok:
List<Number> numList = ...;
List<Integer> intList = ...;
Collections.copy(numList, intList);

// ERROR: cannot copy List<Number> to List<Integer>:
Collections.copy(intList, numList);

All of this is achieved through the super and extends wildcards, enforced by the compiler.

PECS Principle

When to use extends and when to use super? To help remember, we can use the PECS principle: Producer Extends Consumer Super.

This means that if you need to return T, it is a producer, and you should use the extends wildcard; if you need to write T, it is a consumer, and you should use the super wildcard.

Taking the copy() method in Collections as an example:

java
public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i = 0; i < src.size(); i++) {
            T t = src.get(i); // src is a producer
            dest.add(t); // dest is a consumer
        }
    }
}

Since src, which needs to return T, is a producer, it is declared as List<? extends T>. Conversely, since dest, which needs to write T, is a consumer, it is declared as List<? super T>.

Unbounded Wildcard

We have discussed the roles of <? extends T> and <? super T> as method parameters. In fact, Java generics also allow the use of unbounded wildcards (Unbounded Wildcard Type), which is simply defined as ?:

java
void sample(Pair<?> p) {
}

Since the <?> wildcard has neither extends nor super, it has the following restrictions:

  • You cannot call the set(T) method with a reference (except for null).
  • You cannot call the T get() method to retrieve a T reference (you can only get an Object reference).

In other words, you cannot read or write, so you can only perform some null checks:

java
static boolean isNull(Pair<?> p) {
    return p.getFirst() == null || p.getLast() == null;
}

In most cases, you can introduce a generic parameter <T> to eliminate the <?> wildcard:

java
static <T> boolean isNull(Pair<T> p) {
    return p.getFirst() == null || p.getLast() == null;
}

The <?> wildcard has a unique characteristic: Pair<?> is a supertype of all Pair<T>:

java
public class Main {
    public static void main(String[] args) {
        Pair<Integer> p = new Pair<>(123, 456);
        Pair<?> p2 = p; // Safe upcasting
        System.out.println(p2.getFirst() + ", " + p2.getLast());
    }
}

class Pair<T> {
    private T first;
    private T last;

    public Pair(T first, T last) {
        this.first = first;
        this.last = last;
    }

    public T getFirst() {
        return first;
    }
    public T getLast() {
        return last;
    }
    public void setFirst(T first) {
        this.first = first;
    }
    public void setLast(T last) {
        this.last = last;
    }
}

The above code compiles and runs correctly because Pair<Integer> is a subclass of Pair<?>, allowing for safe upcasting.

Summary

Using a wildcard like <? super Integer> as a method parameter indicates:

  • The method can call methods that accept an Integer reference, for example: obj.setFirst(Integer n);.
  • The method cannot call methods that retrieve an Integer reference (except for Object), for example: Integer n = obj.getFirst();.

In other words, using the super wildcard means you can write but not read.

When using extends and super wildcards, you should follow the PECS principle.

The unbounded wildcard <?> is rarely used and can often be replaced with <T>, and it serves as a supertype for all types <T>.

The super Wildcard has loaded