Reading — step 1 of 7
Learn
Effective Java spends Items 26–33 on generics. The basics (List<String>) are easy. The hard part is bounded types and wildcards — and they're inescapable once you start writing reusable APIs.
Type erasure
Java generics are erased at runtime. List<String> and List<Integer> are the same List class — the type parameter exists only at compile time. Implications:
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
strings.getClass() == ints.getClass(); // true — both ArrayList.class
- You can't write
new T[10]— the type is erased - You can't
instanceof List<String>— onlyinstanceof List<?> - Generic exceptions can't be thrown (catch needs the actual type)
Different from C++ templates (per-instantiation code) and Rust generics (monomorphized). Java's approach trades runtime info for backward compatibility.
Bounded type parameters
Constrain a type parameter to a hierarchy:
public static <T extends Comparable<T>> T max(List<T> list) {
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) > 0) result = item;
}
return result;
}
<T extends Comparable<T>> says "T must implement Comparable<T>." Now we can call compareTo on it.
Multiple bounds:
<T extends Number & Comparable<T>>
Class first if there's one (only one class allowed), interfaces after.
Wildcards: ?, extends, super
The trickiest part of Java generics. Three forms:
List<?> // unknown — read Object only, can't add (except null)
List<? extends T> // some subtype of T — read T's, can't add
List<? super T> // some supertype of T — add T's, read Object
PECS — Producer Extends, Consumer Super (Effective Java Item 31):
- If a parameter PRODUCES values for you to read, use
? extends T - If it CONSUMES values you provide, use
? super T
// Producer: reads from src, writes into dest
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (T t : src) dest.add(t);
}
Now copy(numberList, integerList) works — Number is a super of Integer.
Without wildcards, you'd be stuck with exact-type matches. Wildcards are how Collections.copy works for any compatible pair.
Generic methods
Methods can have their own type parameters, separate from the class:
public class Util {
public static <T> List<T> singleton(T t) {
List<T> list = new ArrayList<>();
list.add(t);
return list;
}
}
List<String> s = Util.singleton("hi"); // T inferred as String
List<Integer> i = Util.<Integer>singleton(42); // explicit
The <T> between modifiers and return type declares a method-level type parameter.
Common mistakes
- Raw types (
Listinstead ofList<String>) — defeats the type system. Compiler warns but allows. Never write raw types in new code. - Misusing super/extends — leads to either won't-compile or surprising restrictions. Apply PECS.
- Trying to create T[] directly — erased; can't do it. Use
Object[]cast or(T[]) Array.newInstance(cls, size). List<Object>vsList<?>— they're DIFFERENT.List<Object>accepts Objects;List<?>accepts any list (read-only essentially).- Inheritance variance confusion —
List<Integer>is NOT aList<Number>. Generics are invariant. Wildcards introduce variance.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…