Bounded Types and Wildcards in Java
Bounds restrict what a type parameter may be, and wildcards let a method accept a family of parameterised types safely.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Bounded type parameters
public static <T extends Number> double total(List<T> values) {
double sum = 0;
for (T value : values) {
sum += value.doubleValue(); // available because T is a Number
}
return sum;
}Without the bound, T would be treated as Object and doubleValue() would not compile. A bound both restricts what callers may pass and grants access to the members of the bound.
total(List.of(1, 2, 3)); // Integer extends Number, fine
total(List.of(1.5, 2.5)); // Double, fine
// total(List.of("a", "b")); // compile errorMultiple bounds
public static <T extends Comparable<T> & Serializable> T largest(List<T> values) {
T best = values.get(0);
for (T value : values) {
if (value.compareTo(best) > 0) {
best = value;
}
}
return best;
}Use & to require several bounds. A class bound, if there is one, must come first; the rest are interfaces. Note that extends is used for interfaces here too.
Wildcards
A wildcard ? stands for "some unknown type". It appears in the use of a generic type, not in its declaration.
Unbounded wildcard
public static void printAll(Collection<?> items) {
for (Object item : items) { // safe: everything is an Object
System.out.println(item);
}
// items.add("x"); // not allowed: the element type is unknown
}printAll(List.of("a", "b"));
printAll(Set.of(1, 2, 3)); // both acceptedUpper bounded wildcard: extends
public static double sum(List<? extends Number> values) {
double total = 0;
for (Number value : values) { // reading as Number is safe
total += value.doubleValue();
}
return total;
}sum(List.of(1, 2, 3)); // List<Integer>
sum(List.of(1.5, 2.5)); // List<Double>
// values.add(1); // still not allowed inside the methodYou can read, but you cannot write. The list might be a List<Double>, so adding an Integer would corrupt it.
Lower bounded wildcard: super
public static void addNumbers(List<? super Integer> target) {
target.add(1);
target.add(2);
// Integer first = target.get(0); // not allowed: only Object is guaranteed
}addNumbers(new ArrayList<Integer>());
addNumbers(new ArrayList<Number>());
addNumbers(new ArrayList<Object>()); // all acceptedYou can write, but reading gives you only Object. The list is at least able to hold integers, so adding one is always safe.
PECS: Producer Extends, Consumer Super
| The parameter | Use | You can |
|---|---|---|
| Produces values you read | ? extends T | Read as T, not write |
| Consumes values you write | ? super T | Write T, read only as Object |
| Both | Plain T | Both |
public static <T> void copy(List<? extends T> source, List<? super T> target) {
for (T item : source) { // source produces
target.add(item); // target consumes
}
}List<Integer> numbers = List.of(1, 2, 3);
List<Object> destination = new ArrayList<>();
copy(numbers, destination); // accepted, and type safePECS is the rule to memorise. If a parameter is a source of data useextends; if it is a destination usesuper. The standard library follows it everywhere, which is whyCollections.copyandStream.maphave the signatures they do.
Where you meet it in the library
// Comparator: consumes T, so super
list.sort(Comparator<? super String> comparator);
// Stream.map: the mapper consumes T and produces R
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// forEach: the action consumes T
void forEach(Consumer<? super T> action);These signatures look intimidating until PECS makes them obvious: every parameter is marked by whether it produces or consumes.
Wildcard compared with a type parameter
// A wildcard: simplest when the type is used once
public static int size(Collection<?> items) {
return items.size();
}
// A type parameter: needed when the type appears more than once
public static <T> void swapEnds(List<T> items) {
T first = items.get(0);
items.set(0, items.get(items.size() - 1));
items.set(items.size() - 1, first);
}Use a wildcard when the unknown type appears in exactly one place. Use a named parameter when two positions must agree.
Common mistakes
- Trying to add to a
List<? extends T>. - Expecting to read a specific type from a
List<? super T>. - Writing
List<?>whereList<Object>was meant. They are different:List<Object>accepts writes,List<?>does not. - Using a wildcard in a return type, which pushes the awkwardness onto every caller.
- Using
superwhereextendswas needed, and finding that reads no longer type check.
Best practices
- Apply PECS to every generic parameter of a public method.
- Prefer a wildcard for a type used once, a named parameter otherwise.
- Avoid wildcards in return types.
- Add a bound when the method needs members of a particular type.
- Keep
nullin mind: it is the only value assignable to a? extends Tposition.
Practice
- Write a method that sums any
Collectionof numbers, and explain the wildcard you chose. - Why is
list.add(1)rejected forList<? extends Number>but accepted forList<? super Integer>? - Explain the signature of
Stream.mapusing PECS. - Give an example where
List<?>andList<Object>behave differently. - Write a
copyAllmethod with the correct bounds and test it with a list of integers into a list of objects.
Conclusion
Bounds tell the compiler what a type parameter can do; wildcards let a method accept a family of parameterised types. Remember PECS, and most generic signatures in the standard library become readable.