Type bounds and wildcards are the two ways Java lets you describe how much freedom a generic API gives its caller. A bound written in the type parameter - <T extends Number> - is a promise about what the type can be. A wildcard written in the argument - List<?> - is a promise about what the method can accept. Knowing which one you are making is the difference between an API that is flexible and one that is needlessly rigid.
Why would you bound a type at all?
Consider a container that should hold numbers. Without a bound you would write class Box<T>, and inside the class you could not call doubleValue() on the stored value, because the compiler only knows it is some unknown T. The bound tells the compiler that T is always a Number, and every Number method becomes available.
public class BoundedType {
static class NumberBox<T extends Number> {
private final T value;
NumberBox(T value) {
this.value = value;
}
double asDouble() {
return value.doubleValue();
}
T get() {
return value;
}
}
public static void main(String[] args) {
NumberBox<Integer> whole = new NumberBox<>(42);
NumberBox<Double> decimal = new NumberBox<>(3.5);
System.out.println("whole : " + whole.asDouble());
System.out.println("decimal : " + decimal.asDouble());
System.out.println("sum : " + (whole.asDouble() + decimal.asDouble()));
}
}whole : 42.0
decimal : 3.5
sum : 45.5Notice what the bound did not do. get() still returns the precise type, Integer in one case and Double in the other, so the caller keeps full type information. A bound adds capabilities to the implementation without taking anything away from the caller. That asymmetry is the key difference between a bound and a wildcard.
Can a type have more than one bound?
Yes, as long as at most one of them is a class - the class bound has to come first. Additional interfaces follow after an ampersand. This is how you say "any type that is both a Comparable and has a name".
import java.util.List;
public class MultipleBounds {
interface Named {
String name();
}
static class Person implements Named, Comparable<Person> {
private final String name;
Person(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public int compareTo(Person other) {
return name.compareTo(other.name);
}
}
static <T extends Named & Comparable<T>> T pickBest(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
public static void main(String[] args) {
Person first = new Person("ana");
Person second = new Person("bo");
System.out.println("best : " + pickBest(List.of(first, second)).name());
}
}best : boInside pickBest the compiler now allows compareTo to be called on the element, which is exactly what the bound promised. The price is that the caller must supply elements satisfying both constraints, so the method is less general than one that takes a plain List<T>. Bounds are always a trade: more power inside, less flexibility outside.
What does a wildcard actually say?
A wildcard appears where you would put a type argument, and it describes the relationship between the list's element type and the argument type. List<?> is a list of something - the method accepts any list at all. List<String> is a list of exactly strings. List<? extends Number> is a list of numbers or of some subtype of numbers.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Wildcards {
public static void main(String[] args) {
List<String> names = new ArrayList<>(Arrays.asList("ana", "bo"));
List<Object> everything = new ArrayList<>(List.of(1, "two", 3.0));
printAll(names);
printAll(everything);
System.out.println("size of names : " + count(names));
try {
addTo(names);
} catch (UnsupportedOperationException e) {
System.out.println("cannot add to a fixed list : " + e.getClass().getSimpleName());
}
}
static void printAll(List<?> items) {
for (Object item : items) {
System.out.println("item : " + item);
}
}
static int count(List<?> items) {
return items.size();
}
static void addTo(List<String> items) {
items.add("cy");
}
}item : ana
item : bo
item : 1
item : two
item : 3.0
size of names : 2Inside printAll the element type is unknown, so the only safe thing to do is read the elements as Object. You cannot add anything to a List<?>, because the compiler cannot prove that your value matches the unknown element type. That restriction is not a limitation of the syntax - it is the whole point. A method that accepts List<?> promises only to look at the contents.
Compare that with addTo, which takes a List<String>. The compiler allows it to add a string, and the runtime refuses because the list it was handed was created by Arrays.asList, which does not support additions. That is the well-known trap of Arrays.asList: fixed size, but individual elements can be replaced.
What is the PECS rule?
Producer Extends, Consumer Super. It is a one-line guide to choosing between the three wildcard forms, and it pays for itself the first time you write a method that both reads from and writes to a collection.
import java.util.ArrayList;
import java.util.List;
public class Pecs {
public static void main(String[] args) {
List<Object> objects = new ArrayList<>();
List<String> strings = new ArrayList<>(List.of("ana", "bo"));
List<Integer> numbers = new ArrayList<>(List.of(1, 2));
copy(objects, strings);
copy(objects, numbers);
System.out.println("objects : " + objects);
System.out.println("strings still empty : " + strings);
}
static <T> void copy(List<? super T> target, List<? extends T> source) {
for (T item : source) {
target.add(item);
}
}
}objects : [ana, bo, 1, 2]
strings still empty : [ana, bo]Read the signature as two separate promises. The parameter List<? extends T> source is a producer: you may read T values out of it. The parameter List<? super T> target is a consumer: you may write T values into it. Using them the other way round would be a compile error, and the compiler will tell you exactly that - which is the whole benefit of expressing the relationship in the type.
Try to write copy(strings, objects) and the compiler refuses: a List<Object> might contain anything, so putting its contents into a List<String> cannot be type safe. The wildcard signature is what makes that mistake impossible rather than merely discouraged.
| Position | Use | Why |
|---|---|---|
| Type parameter bound | <T extends Number> | The implementation needs the extra methods |
| Argument, read only | List<? extends T> | You take values out, so any subtype works |
| Argument, write only | List<? super T> | You put values in, so any supertype works |
| Argument, read and write | List<T> | The element type has to match exactly |
| Argument, no relationship | List<?> | You only inspect the contents |
How do you choose in practice?
- Your method returns the elements - use
? extends T. - Your method adds elements - use
? super T. - Your method only counts or prints - use
?. - Your class needs to call methods on the stored value - use a bound on the type parameter.
- Your class stores the value and hands it back with full type information - bound the parameter, not the argument.
One last habit worth forming: when a wildcard forces you to cast inside a method, that is usually a sign the signature should be different. A method that takes List<?> and then casts every element to a concrete type has pushed a guarantee the compiler could have made for you into your own code.
The rules to remember
- A bound on the type parameter gives the implementation power and keeps the caller precise.
- A wildcard on an argument gives the caller freedom and takes precision away from the method.
- At most one class bound is allowed, and it comes first.
- Producers use
extends, consumers usesuper. - You cannot add to a
List<?>, and that is a safety feature, not a limitation.
Try it yourself: write a method double total(List<? extends Number> values) that sums any list of numbers. Add a second method void addAll(List<? super Integer> target, List<Integer> source) that copies integers into any list that can hold them. Then try to write a method that both reads and writes the same list and see which wildcard the compiler demands for each side.