JAVA / GENERICS AND TYPE ERASURE
Upper versus lower bounds in practice
Choose between ? extends and ? super for each parameter of a real signature, knowing exactly which operation each bound enables and which it removes.
What you will learn
- Mark read-only parameters ? extends T and write-only parameters ? super T
- Write copy(List<? extends T> src, List<? super T> dst) and justify each bound
- Know why get on List<? super Integer> yields Object and add on ? extends is rejected
- Use Comparator<? super T> and Comparable<? super T> so inherited orderings still fit
Understanding Upper versus lower bounds in practice
A bound on a wildcard is a statement about what the compiler knows, and each direction of knowledge enables exactly one direction of data flow. List<? extends Number> says the element type is one unknown subtype of Number, so everything coming out is at least a Number, while nothing you are holding is provably that unknown type; that is why get is usefully typed and add is refused. List<? super Integer> reverses the situation: the element type is one unknown supertype of Integer, so any Integer you have is safe to put in, but on the way out the strongest surviving guarantee is Object.
The two forms are not legal in the same places. A type parameter declaration accepts only upper bounds, as in <T extends Number & Comparable<T>>, and <T super Number> is not Java at all. An upper bound earns its place at the declaration because it tells the compiler which methods T supports inside the body, whereas a lower bound supplies no methods whatsoever, so it is only meaningful as a claim about a particular use site. That is why you meet lower bounds in parameter types such as List<? super T>, Comparator<? super T> and Consumer<? super T>, and never as a constraint on a class's own type variable.
The practical decision is made per parameter, not per method, which is why a transfer method carries both bounds at once, one on each side. When a single parameter must be both read as T and written as T, neither wildcard works and plain List<T> is the correct choice; treating that as a failure of wildcards leads people to cast. Keep wildcards out of return types for the same reason: handing back List<? extends Number> gives the caller a list they cannot add to, and handing back a ? super type forces casts at every call site.
import java.util.ArrayList;
import java.util.List;
public class Bounds {
// src is only read from, dst is only written to
static <T> void copy(List<? extends T> src, List<? super T> dst) {
for (T item : src) {
dst.add(item);
}
}
public static void main(String[] args) {
List<Integer> ints = new ArrayList<>(List.of(1, 2, 3));
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
copy(ints, numbers); // T = Integer
copy(numbers, objects); // T = Number
List<? extends Number> producer = ints;
Number read = producer.get(0); // typed read, no cast needed
// producer.add(4); // rejected: element type is unknown
List<? super Integer> consumer = objects;
consumer.add(42); // typed write
Object back = consumer.get(consumer.size() - 1); // only Object survives
System.out.println("numbers = " + numbers);
System.out.println("objects = " + objects);
System.out.println("read = " + read.intValue());
System.out.println("back = " + back);
}
}An upper bound buys typed reads at the cost of writes and a lower bound buys typed writes at the cost of reads, so you pick one per parameter according to which way values move.
Worked examples
A supertype comparator for a subtype list
Shows why an ordering parameter is declared Comparator<? super T> while the data parameter is List<? extends T>.
import java.util.Comparator;
import java.util.List;
public class MaxOf {
static <T> T maxOf(List<? extends T> items, Comparator<? super T> order) {
T best = items.get(0);
for (T item : items) {
if (order.compare(item, best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
Comparator<Number> byValue = Comparator.comparingDouble(Number::doubleValue);
List<Integer> ints = List.of(3, 17, 8);
List<Double> doubles = List.of(2.5, 9.75, 4.0);
Integer biggestInt = maxOf(ints, byValue);
Double biggestDouble = maxOf(doubles, byValue);
System.out.println(biggestInt);
System.out.println(biggestDouble);
}
}Example explained
Line 1List<? extends T> is enough because maxOf only pulls elements out, which is what lets one method take List<Integer> and List<Double>.
Line 2Comparator<? super T> accepts Comparator<Number> when T is Integer: anything able to order Numbers can order Integers.
Line 3Declared as Comparator<T>, the first call would not compile, because Comparator<Number> is not a Comparator<Integer>.
Line 4Inference still settles on T = Integer rather than Number, so biggestInt needs no cast.
Why the JDK writes Comparable<? super T>
Demonstrates a lower bound nested inside an upper bound so a subclass that inherits compareTo still satisfies the constraint.
import java.util.List;
public class NaturalMax {
static <T extends Comparable<? super T>> T max(List<? extends T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
static class Animal implements Comparable<Animal> {
final String name;
final int grams;
Animal(String name, int grams) {
this.name = name;
this.grams = grams;
}
@Override
public int compareTo(Animal other) {
return Integer.compare(grams, other.grams);
}
@Override
public String toString() {
return name;
}
}
static class Dog extends Animal {
Dog(String name, int grams) {
super(name, grams);
}
}
public static void main(String[] args) {
List<Dog> dogs = List.of(new Dog("rex", 30000), new Dog("pip", 8000));
Dog heaviest = max(dogs);
List<String> words = List.of("pear", "apple", "fig");
String lastWord = max(words);
System.out.println(heaviest);
System.out.println(lastWord);
}
}Example explained
Line 1Dog inherits compareTo from Animal, so its Comparable type argument is Animal, not Dog.
Line 2Comparable<? super T> accepts exactly that: an ordering defined on Animal is a valid ordering for Dog, so T resolves to Dog and heaviest needs no cast.
Line 3Bounded as <T extends Comparable<T>>, the max(dogs) call would be rejected, since Dog is not a Comparable<Dog>.
Line 4Inside the body item.compareTo(best) compiles because compareTo's parameter is some supertype of T, and best is a T.
Important notes
null is the only value you may add to a List<? extends Number>, because null converts to every reference type; that is a technicality, not a way around the bound.
? super Integer does not mean any supertype you fancy: the element type is one unknown supertype, so an Integer can be added but a Number cannot, since the list might really be a List<Integer>.
Common mistakes
Writing <T super Integer> on a method or class: the compiler stops at super, because only wildcards take lower bounds, and the constraint has to move into a parameter type such as List<? super T>.
Declaring a destination as List<? extends T> and then calling dst.add(item): the add does not compile, and the usual rescue of casting dst to a raw List trades a real error for an unchecked one.
Assuming get on a List<? super Integer> returns an Integer and inserting a cast to force it: the list may really be a List<Object> holding a String, so the cast throws ClassCastException at runtime.
Try it yourself
Change, predict, then run
Write static <T> void drain(List<? extends T> src, List<? super T> dst) that copies every element of src into dst, then call it with a List<Integer> source and a List<Object> destination and print the destination. Now swap the two bounds and note which single statement the compiler rejects.
Open the Java workspaceCheck your understanding
A method must read elements of type T out of a list parameter and also store a newly created T back into that same list. Which parameter type should it declare?
- List<? extends T>
- List<? super T>
- List<T>
- List<?>
Show answer
Each wildcard bound supports only one direction, so needing both rules them out: plain List<T> is the only type that gives a typed get and a typed add. List<? super T> is tempting because add(T) works, but get returns Object, so the reading half of the method breaks; List<? extends T> fails the other way, rejecting the add.