Generics in Java provide type safety, reusability, and maintainability. A key part of generics is bounded wildcards using extends
and super
. These allow developers to restrict what types can be used while still maintaining flexibility in APIs.
The common confusion is knowing when to use ? extends T
(upper bound) vs ? super T
(lower bound). This is where the PECS principle (Producer Extends, Consumer Super) comes in.
Think of it this way:
extends
is like a vending machine (you can take items out, but not put new ones in).super
is like a donation box (you can put items in, but can’t guarantee what you’ll get out).
This tutorial explores upper and lower bounds, explains their differences, and demonstrates real-world applications.
Core Definition and Purpose of Java Generics
Generics allow code to be parameterized with types, leading to safer, cleaner, and reusable APIs. Bounds (extends
, super
) refine these generic parameters by restricting or broadening the type hierarchy.
Introduction to Type Parameters: <T>
, <E>
, <K, V>
<T>
– General type parameter.<E>
– Element type (used in collections).<K, V>
– Key-Value pairs (used in maps).
Upper Bounded Wildcards: ? extends T
Upper bounds restrict a wildcard to a type or its subtypes.
public static void printList(List<? extends Number> list) {
for (Number n : list) {
System.out.println(n);
}
}
Usage:
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Double> doubles = Arrays.asList(1.1, 2.2, 3.3);
printList(ints);
printList(doubles);
- Safe for reading (
Producer Extends
). - Cannot add new elements except
null
.
Lower Bounded Wildcards: ? super T
Lower bounds restrict a wildcard to a type or its supertypes.
public static void addNumbers(List<? super Integer> list) {
list.add(42);
list.add(100);
}
Usage:
List<Integer> ints = new ArrayList<>();
List<Number> nums = new ArrayList<>();
addNumbers(ints);
addNumbers(nums);
- Safe for writing (
Consumer Super
). - Reading returns
Object
, since type is unknown.
PECS Principle: Producer Extends, Consumer Super
- Use
extends
when your method produces data (you only read). - Use
super
when your method consumes data (you write).
Example:
public static void copy(List<? extends Number> src, List<? super Number> dest) {
for (Number n : src) dest.add(n);
}
Multiple Type Parameters with Bounds
public static <K extends Comparable<K>, V extends Number> void process(Map<K, V> map) {
for (K key : map.keySet()) {
System.out.println("Key: " + key);
}
}
Wildcards vs Type Parameters
- Type parameters (
<T extends Number>
) are used when designing APIs. - Wildcards (
? extends Number
) are used when consuming existing APIs.
Generics in Collections Framework
Example with List
and extends
List<? extends Number> numbers = Arrays.asList(1, 2.5, 3);
Number n = numbers.get(0); // Safe
// numbers.add(10); // Not allowed
Example with List
and super
List<? super Integer> integers = new ArrayList<Number>();
integers.add(5); // Safe
Object obj = integers.get(0); // Returns Object
Raw Types vs Parameterized Types
List rawList = new ArrayList(); // Unsafe
List<? extends Number> safeList = new ArrayList<Integer>();
Type Inference and Diamond Operator
Map<String, ? extends Number> map = new HashMap<>();
Type Erasure and Bounded Wildcards
At runtime, both ? extends Number
and ? super Integer
are erased to List
. Type checks exist only at compile time.
Reifiable vs Non-Reifiable Types
- Reifiable:
List<?>
. - Non-Reifiable:
List<? extends Number>
.
Recursive Type Bounds
public static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElse(null);
}
Designing Fluent APIs with Bounds
class Builder<T extends Builder<T>> {
public T withName(String name) { return (T) this; }
}
Generics with Enums and Annotations
EnumSet<? extends Enum<?>> days = EnumSet.allOf(Day.class);
Case Studies
Type-Safe Cache
class Cache<K, V> {
private Map<K, V> store = new HashMap<>();
public void put(K key, V value) { store.put(key, value); }
public V get(K key) { return store.get(key); }
}
Flexible Repository Pattern
interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
}
Event Handling System
interface EventListener<E> {
void onEvent(List<? extends E> events);
}
Copy Utility with PECS
public static <T> void copy(List<? extends T> src, List<? super T> dest) {
for (T item : src) dest.add(item);
}
Best Practices for Using extends
and super
- Use
extends
when a method only reads. - Use
super
when a method only writes. - Prefer wildcards in API design.
- Avoid mixing raw types with bounded generics.
Common Anti-Patterns
- Overusing wildcards where type parameters suffice.
- Deeply nested bounds (
<? extends List<? super T>>
). - Ignoring the PECS principle.
Performance Considerations
Bounds (extends
, super
) impose no runtime cost. Checks are enforced at compile time, and erased at runtime.
📌 What's New in Java for Generics?
- Java 5: Generics introduced with
extends
andsuper
. - Java 7: Diamond operator simplified generic instantiation.
- Java 8: Streams and lambdas integrated bounds extensively.
- Java 10:
var
works with generics. - Java 17+: Sealed classes fit naturally with bounded generics.
- Java 21: Virtual threads enhance concurrent collections using bounds.
Conclusion and Key Takeaways
Understanding when to use extends
vs super
is crucial for designing flexible, type-safe APIs. Remember the PECS principle:
- Producer Extends → Use
extends
for reading. - Consumer Super → Use
super
for writing.
By mastering bounded wildcards, developers can avoid ClassCastException
, simplify code, and design scalable APIs.
FAQ on Upper vs Lower Bounds
Q1: Why can’t I add elements to a List<? extends T>
?
Because the compiler cannot guarantee the subtype at runtime.
Q2: Can I always use ? extends Object
instead of ? super T
?
No, super
ensures safe writing, extends
ensures safe reading.
Q3: What’s the difference between <T>
and ? extends T
?<T>
is for defining generic classes/methods; ? extends T
is for consuming them.
Q4: Why does ? super T
return Object
when reading?
Because the lower bound only guarantees it’s some supertype of T.
Q5: Can I mix extends
and super
in one method?
Yes, often used in copy utilities (copy(List<? extends T>, List<? super T>)
).
Q6: Does PECS apply only to collections?
No, it applies anywhere wildcards define input/output behavior.
Q7: How does type erasure affect bounds?
Both ? extends
and ? super
are erased to raw types at runtime.
Q8: Do bounds affect performance?
No, all checks happen at compile time.
Q9: Can I use wildcards with Maps?
Yes, e.g., Map<? extends Number, ? super String>
.
Q10: Should I prefer wildcards or explicit type parameters?
Wildcards are better for API consumers, type parameters for API definitions.