Skip to content
Dev Dump

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.

// Without generics — compiles but blows up at runtime
List raw = new ArrayList();
raw.add("hello");
raw.add(42);
Integer num = (Integer) raw.get(0); // ClassCastException at runtime
// With generics — compiler catches the mistake
List<String> typed = new ArrayList<>();
typed.add("hello");
// typed.add(42); // compile error — caught immediately
String s = typed.get(0); // no cast needed

Restrict what types can be used as a generic argument.

// Only accepts types that implement Comparable
public <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 Comparable
public <T extends Number & Comparable<T>> T findMax(List<T> list) { ... }

Wildcards (?) represent an unknown type. They’re used when a method needs to accept collections of different types without caring about the exact type.

“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")); // works
printAll(List.of(1, 2, 3)); // works

You can read (Object type), but you cannot add anything (except null) — the compiler doesn’t know the actual type.

“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> — works
sum(List.of(1.5, 2.5)); // List<Double> — works

You can read as Number, but you cannot add — the compiler doesn’t know if the list is List<Integer>, List<Double>, etc.

“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>()); // works
addNumbers(new ArrayList<Number>()); // works
addNumbers(new ArrayList<Object>()); // works

You can add Integer values, but reading gives you Object (you don’t know the exact supertype).

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) → super
public <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 ✓

The JDK follows PECS consistently:

// Collections.addAll — collection consumes, so ? super
public static <T> boolean addAll(Collection<? super T> c, T... elements)
// Collections.max — collection produces, so ? extends
public static <T extends Comparable<? super T>> T max(Collection<? extends T> coll)
// Collections.copy — src produces, dest consumes
public static <T> void copy(List<? super T> dest, List<? extends T> src)
You want to…UseCan read asCan write
Read from collection? extends TTNo (except null)
Write to collection? super TObject onlyT and subtypes
Both read and writeT (no wildcard)TT
Neither — just check size, etc.?ObjectNo (except null)

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 parameter
strings.getClass() == ints.getClass(); // true

Consequences:

  • You cannot do new T() or new T[] — the type is unknown at runtime
  • You cannot do instanceof List<String> — only instanceof 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) { }

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]
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));
}
  1. Use <T> when you need both read and write access to the same type
  2. Use ? extends T for read-only parameters (producers)
  3. Use ? super T for write-only parameters (consumers)
  4. Use ? when you don’t need the type at all (just size(), isEmpty(), etc.)
  5. Return types should never use wildcards — it forces callers to deal with ?
  6. Remember PECS: Producer Extends, Consumer Super