Generics
Generics let you parameterize types over the types they operate on, moving an entire class of ClassCastExceptions from runtime to compile time. List<String> is not just documentation — the compiler enforces that only Strings go in and guarantees Strings come out, with no casting at the call site. Generics power every collection in the framework and most well-designed APIs.
Generic Classes
A type parameter is declared in angle brackets after the class name and used like any other type inside the class.
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>();
box.set("hello");
String s = box.get(); // no cast needed
By convention, parameters are single uppercase letters: T (type), E (element), K/V (key/value), R (result), N (number).
Generic Methods
A method can declare its own type parameters, independent of the class, by placing them before the return type.
static <T> T firstOrNull(List<T> items) {
return items.isEmpty() ? null : items.get(0);
}
String first = firstOrNull(List.of("a", "b")); // T inferred as String
The compiler infers T from the arguments, so you rarely specify it explicitly.
Bounded Type Parameters
A bound restricts the type argument to a subtype, unlocking the methods of that supertype inside the generic code. T extends Comparable<T> is the classic bound for “anything orderable.”
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item; // compareTo available via bound
}
return best;
}
System.out.println(max(List.of(3, 9, 1))); // 9
Output:
9
Multiple bounds are joined with &: <T extends Number & Comparable<T>>.
Wildcards and PECS
Generics are invariant: List<Integer> is not a List<Number>. Wildcards reintroduce flexibility for method parameters.
? extends T— an upper-bounded wildcard: you can readTs out, but cannot add (a producer).? super T— a lower-bounded wildcard: you can addTs in, but reads come back asObject(a consumer).
The mnemonic is PECS — Producer Extends, Consumer Super.
// Producer: source only yields Numbers -> use extends
static double sum(List<? extends Number> source) {
double total = 0;
for (Number n : source) total += n.doubleValue();
return total;
}
// Consumer: sink only receives Integers -> use super
static void fill(List<? super Integer> sink, int count) {
for (int i = 0; i < count; i++) sink.add(i);
}
System.out.println(sum(List.of(1, 2.5, 3L))); // 6.5
List<Number> nums = new ArrayList<>();
fill(nums, 3); // [0, 1, 2]
System.out.println(nums);
Output:
6.5
[0, 1, 2]
PECS in one line: if a parameter produces values you read, use
? extends; if it consumes values you write, use? super. The unbounded?is for code that neither reads typed values nor writes.
Type Erasure
Generics are a compile-time construct. The compiler verifies type usage, inserts casts, then erases type parameters — List<String> and List<Integer> are both just List at runtime. Consequences:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
System.out.println(a.getClass() == b.getClass()); // true — same raw class
Output:
true
Erasure means you cannot: use new T[] or new T(), check obj instanceof List<String>, or overload methods that differ only by generic type. Capture a Class<T> token or a factory if you need the runtime type.
Benefits
- Compile-time safety — type errors surface at build time, not as production
ClassCastExceptions. - No manual casts — callers get the right type back directly.
- Self-documenting APIs —
Map<UserId, Session>states intent thatMapalone cannot. - Reusable algorithms — one generic method works across every element type.
Best Practices
- Prefer generic types and methods over raw types (
Listwithout<>) andObjectcasting. - Apply PECS to every parameter:
extendsfor producers,superfor consumers. - Bound type parameters (
<T extends Comparable<T>>) instead of casting inside the method. - Use bounded wildcards on parameters, but plain type variables on return types for usability.
- Never suppress
uncheckedwarnings without isolating and commenting the one line that needs it.
Interview Questions
Q: What is type erasure and why does Java use it?
A: Erasure removes generic type information after compilation, replacing type parameters with their bounds (or Object) and inserting casts. Java chose it for migration compatibility — generic and pre-generics code interoperate, and no new bytecode/JVM changes were required.
Q: Explain PECS.
A: Producer Extends, Consumer Super. Use ? extends T when a structure only produces (you read Ts from it); use ? super T when it only consumes (you write Ts into it). It maximizes the set of types a method accepts while staying type-safe.
Q: Why can’t you create an array of a generic type, e.g. new T[10]?
A: Arrays are reified (they know their element type at runtime) but generics are erased. The runtime couldn’t perform the array’s store-check against T, so the language forbids new T[]. The workaround is an Object[] with an unchecked cast, or Array.newInstance with a Class<T> token.
Q: What is the difference between List<Object> and List<?>?
A: List<Object> is a concrete list you can add any Object to. List<?> is an unknown but specific type — you can read elements as Object but cannot add anything except null, because the compiler can’t prove your element matches the (unknown) actual type.