Generics & Wildcards
Generics let you write collections and methods that are type-safe at compile time. Without them, you’d be casting Object everywhere and discovering type errors at runtime.
Why Generics Matter for Collections
Section titled “Why Generics Matter for Collections”// Without generics — compiles but blows up at runtimeList raw = new ArrayList();raw.add("hello");raw.add(42);Integer num = (Integer) raw.get(0); // ClassCastException at runtime
// With generics — compiler catches the mistakeList<String> typed = new ArrayList<>();typed.add("hello");// typed.add(42); // compile error — caught immediatelyString s = typed.get(0); // no cast neededBounded Type Parameters
Section titled “Bounded Type Parameters”Restrict what types can be used as a generic argument.
// Only accepts types that implement Comparablepublic <T extends Comparable<T>> T findMax(List<T> list) { T max = list.get(0); for (T item : list) { if (item.compareTo(max) > 0) max = item; } return max;}
// Multiple bounds — must extend Number AND implement Comparablepublic <T extends Number & Comparable<T>> T findMax(List<T> list) { ... }Wildcards
Section titled “Wildcards”Wildcards (?) represent an unknown type. They’re used when a method needs to accept collections of different types without caring about the exact type.
Unbounded Wildcard: <?>
Section titled “Unbounded Wildcard: <?>”“I don’t care what’s in this collection — I just want to read objects from it.”
public void printAll(List<?> list) { for (Object item : list) { System.out.println(item); }}
printAll(List.of("a", "b")); // worksprintAll(List.of(1, 2, 3)); // worksYou can read (Object type), but you cannot add anything (except null) — the compiler doesn’t know the actual type.
Upper-Bounded Wildcard: <? extends T>
Section titled “Upper-Bounded Wildcard: <? extends T>”“Anything that is a T or a subclass of T.” — used when you read from a collection.
public double sum(List<? extends Number> numbers) { double total = 0; for (Number n : numbers) { total += n.doubleValue(); } return total;}
sum(List.of(1, 2, 3)); // List<Integer> — workssum(List.of(1.5, 2.5)); // List<Double> — worksYou can read as Number, but you cannot add — the compiler doesn’t know if the list is List<Integer>, List<Double>, etc.
Lower-Bounded Wildcard: <? super T>
Section titled “Lower-Bounded Wildcard: <? super T>”“Anything that is a T or a superclass of T.” — used when you write to a collection.
public void addNumbers(List<? super Integer> list) { list.add(1); list.add(2); list.add(3);}
addNumbers(new ArrayList<Integer>()); // worksaddNumbers(new ArrayList<Number>()); // worksaddNumbers(new ArrayList<Object>()); // worksYou can add Integer values, but reading gives you Object (you don’t know the exact supertype).
PECS — Producer Extends, Consumer Super
Section titled “PECS — Producer Extends, Consumer Super”The key rule for deciding which wildcard to use:
If the collection produces data you read →
extends
If the collection consumes data you write →super
// src PRODUCES elements (we read from it) → extends// dest CONSUMES elements (we write to it) → superpublic <T> void copy(List<? extends T> src, List<? super T> dest) { for (T item : src) { dest.add(item); }}
List<Integer> ints = List.of(1, 2, 3);List<Number> nums = new ArrayList<>();copy(ints, nums); // Integer extends Number ✓PECS in the Standard Library
Section titled “PECS in the Standard Library”The JDK follows PECS consistently:
// Collections.addAll — collection consumes, so ? superpublic static <T> boolean addAll(Collection<? super T> c, T... elements)
// Collections.max — collection produces, so ? extendspublic static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// Collections.copy — src produces, dest consumespublic static <T> void copy(List<? super T> dest, List<? extends T> src)Quick Reference
Section titled “Quick Reference”| You want to… | Use | Can read as | Can write |
|---|---|---|---|
| Read from collection | ? extends T | T | No (except null) |
| Write to collection | ? super T | Object only | T and subtypes |
| Both read and write | T (no wildcard) | T | T |
| Neither — just check size, etc. | ? | Object | No (except null) |
Type Erasure
Section titled “Type Erasure”Generics are a compile-time feature only. At runtime, the JVM sees raw types — all generic type info is erased.
List<String> strings = new ArrayList<>();List<Integer> ints = new ArrayList<>();
// At runtime, both are just ArrayList — no type parameterstrings.getClass() == ints.getClass(); // trueConsequences:
- You cannot do
new T()ornew T[]— the type is unknown at runtime - You cannot do
instanceof List<String>— onlyinstanceof List<?> - You cannot overload methods that differ only in generic type:
// COMPILE ERROR — both erase to process(List)void process(List<String> list) { }void process(List<Integer> list) { }Common Patterns
Section titled “Common Patterns”Generic method that returns a typed result
Section titled “Generic method that returns a typed result”public <T> List<T> filterByType(List<?> list, Class<T> type) { List<T> result = new ArrayList<>(); for (Object item : list) { if (type.isInstance(item)) { result.add(type.cast(item)); } } return result;}
List<Number> mixed = List.of(1, 2.5, 3, 4.0);List<Integer> ints = filterByType(mixed, Integer.class); // [1, 3]Type-safe heterogeneous container
Section titled “Type-safe heterogeneous container”Map<Class<?>, Object> registry = new HashMap<>();
public <T> void put(Class<T> type, T value) { registry.put(type, value);}
public <T> T get(Class<T> type) { return type.cast(registry.get(type));}Rules of Thumb
Section titled “Rules of Thumb”- Use
<T>when you need both read and write access to the same type - Use
? extends Tfor read-only parameters (producers) - Use
? super Tfor write-only parameters (consumers) - Use
?when you don’t need the type at all (justsize(),isEmpty(), etc.) - Return types should never use wildcards — it forces callers to deal with
? - Remember PECS: Producer Extends, Consumer Super