- Understand upper and lower bounded wildcards
- Master the PECS principle (Producer Extends, Consumer Super)
- Apply wildcards in real-world generic scenarios
- Choose between bounded type parameters and wildcards appropriately
Type Bounds and Wildcards in Java Generics
Generic type parameters can be any type. But what if you need a method that works with numbers only? Or a collection accepting any subtype of a class? That's where type bounds and wildcards come in.
They constrain what types work with your generic code, letting you call specific methods while keeping flexibility.
The Need for Bounds
Try finding the maximum of two values:
public static <T> T max(T a, T b) {
if (a > b) return a; // Error! Can't use > on generic type T
return b;
}
This won't compile because the compiler doesn't know if T supports the > operator. We need to tell the compiler that T must be a type that can be compared - in other words, T must have a bound.
Upper Bounds with extends
An upper bound restricts the type parameter to a specific class or interface and its subtypes. The keyword extends is used for both classes and interfaces.
Bounded Type Parameters
// T must be Comparable or a subtype
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Now the compiler knows:
Tmust implementComparable<T>- We can call
compareTo()on values of typeT
Usage:
String result = max("apple", "banana"); // Works - String is Comparable
Integer bigger = max(10, 20); // Works - Integer is Comparable
// max(new Object(), new Object()); // Error - Object isn't Comparable
Multiple Bounds
A type parameter can have multiple bounds using &:
// T must extend Number AND implement Comparable
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
Rules for multiple bounds:
- At most one class bound (must be listed first)
- Any number of interface bounds
- Syntax:
<T extends Class & Interface1 & Interface2>
// Works with Integer (extends Number, implements Comparable)
Integer max = max(10, 20);
// Works with Double
Double maxDouble = max(1.5, 2.5);
// Doesn't work with AtomicInteger (extends Number but not Comparable)
// AtomicInteger maxAtomic = max(new AtomicInteger(1), new AtomicInteger(2)); // Error!
Wildcards: The Unknown Type
While bounded type parameters work well for declaring generic classes and methods, wildcards (?) are used when you're using generic types and need flexibility in what you accept.
The Problem Wildcards Solve
Consider this seemingly reasonable code:
public static void printList(List<Object> list) {
for (Object item : list) {
System.out.println(item);
}
}
List<String> strings = Arrays.asList("a", "b", "c");
printList(strings); // Error! List<String> is NOT a List<Object>
This fails because List<String> is NOT a subtype of List<Object>, even though String is a subtype of Object. This is called invariance of generic types.
Wildcards solve this problem.
Unbounded Wildcard (?)
The unbounded wildcard matches any type:
public static void printList(List<?> list) {
for (Object item : list) { // Can read as Object
System.out.println(item);
}
}
// Now all of these work:
printList(Arrays.asList("a", "b", "c")); // List<String>
printList(Arrays.asList(1, 2, 3)); // List<Integer>
printList(Arrays.asList(new Date())); // List<Date>
Important limitation: With List<?>, you can't add elements (except null) because the compiler doesn't know the actual type:
List<?> list = new ArrayList<String>();
list.add("hello"); // Error! Can't add to List<?>
list.add(null); // OK - null is valid for any type
Object o = list.get(0); // OK - can read as Object
Upper Bounded Wildcard (? extends Type)
Use ? extends when you want to read from a generic structure:
// Accept any list of Number or its subtypes (Integer, Double, etc.)
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue(); // Can call Number methods
}
return total;
}
// All of these work:
sum(Arrays.asList(1, 2, 3)); // List<Integer>
sum(Arrays.asList(1.5, 2.5, 3.5)); // List<Double>
sum(Arrays.asList(1L, 2L, 3L)); // List<Long>
Key insight: ? extends Number means "some unknown type that is Number or a subtype of Number."
Limitation: You can't add to a List<? extends Number>:
List<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1); // Error!
numbers.add(1.0); // Error!
// Why? The list could be List<Integer>, List<Double>, or any Number subtype
// Adding Integer to what might be List<Double> would be unsafe
Lower Bounded Wildcard (? super Type)
Use ? super when you want to write to a generic structure:
// Accept any list that can hold Integers (List<Integer>, List<Number>, List<Object>)
public static void addNumbers(List<? super Integer> list) {
list.add(1); // Safe - Integer is compatible with Integer, Number, Object
list.add(2);
list.add(3);
}
// All of these work:
List<Integer> integers = new ArrayList<>();
addNumbers(integers); // List<Integer> can hold Integer
List<Number> numbers = new ArrayList<>();
addNumbers(numbers); // List<Number> can hold Integer
List<Object> objects = new ArrayList<>();
addNumbers(objects); // List<Object> can hold Integer
Key insight: ? super Integer means "some unknown type that is Integer or a supertype of Integer."
Limitation: When reading from List<? super Integer>, you can only assume it returns Object:
List<? super Integer> list = new ArrayList<Number>();
list.add(1); // OK - can add Integer
Object o = list.get(0); // Can only read as Object, not Integer
The PECS Principle
The PECS principle (Producer Extends, Consumer Super) helps you remember when to use which wildcard:
- Producer (you read from it) → use
extends - Consumer (you write to it) → use
super
Example: Copying Elements
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T item : src) { // src is a Producer - we read from it
dest.add(item); // dest is a Consumer - we write to it
}
}
// Usage
List<Integer> integers = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(numbers, integers); // Copy integers to numbers list
Collections.sort() Example
The signature of Collections.sort() demonstrates PECS:
public static <T extends Comparable<? super T>> void sort(List<T> list)
Breaking this down:
T extends Comparable<...>- T must be ComparableComparable<? super T>- T can be compared against T or any supertype- This allows
List<Integer>to be sorted even though Integer implementsComparable<Integer>, notComparable<Number>
- This allows
Wildcard Capture
Sometimes you need to "capture" a wildcard into a named type parameter:
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Helper method captures the wildcard
private static <T> void swapHelper(List<T> list, int i, int j) {
T temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
The helper method needs a named type T to store the temporary value. The compiler "captures" the unknown type from List<?> into T.
Choosing Between Type Parameters and Wildcards
Use Type Parameter <T> |
Use Wildcard <?> |
|---|---|
| When the type is used multiple times | When the type is used once |
| When you need to relate types | When types are independent |
| When you need to write to the structure | When you only read (extends) |
// Type parameter - same T used for input and return
public static <T> T getFirst(List<T> list) {
return list.get(0);
}
// Wildcard - type only used once, just printing
public static void printAll(List<?> list) {
for (Object o : list) {
System.out.println(o);
}
}
Type bounds and wildcards give you control. Use <T extends Number> when you need to call Number methods. Use <?> for maximum flexibility but read-only access. Remember PECS: Producer extends (read from), Consumer super (write to). Upper bounds let you read, lower bounds let you write.
