Java generics: type parameters, bounds and erasure
Type parameters, bounded types and generic methods — plus what erasure means for the code you can and cannot write.
Generics — type parameters, bounded types, generic methods
Generics exist because of Day 031's array covariance problem. Today is the mechanism; Day 055 is what it costs; Day 056 is how to use it properly.
L-054 · Generics
The problem they solve
Before Java 5:
List list = new ArrayList();
list.add("hello");
list.add(42); // no complaint
String s = (String) list.get(1); // 💥 ClassCastException at RUNTIME
Every retrieval needed a cast, and every cast was a runtime gamble.
List<String> list = new ArrayList<>();
list.add("hello");
list.add(42); // ❌ COMPILE ERROR
String s = list.get(0); // no cast needed
Generics move type errors from runtime to compile time, and remove the casts. That's the entire value proposition, and it's the answer to "why do generics exist".
Type parameters
public class Box<T> { // T is a TYPE PARAMETER
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
Box<String> stringBox = new Box<>(); // T is bound to String — a TYPE ARGUMENT
Conventional single-letter names, and using them signals familiarity:
| Letter | Means |
|---|---|
T |
Type |
E |
Element (collections) |
K, V |
Key, Value |
N |
Number |
R |
Return type |
S, U |
Second, third types |
The diamond <> (Java 7+) infers the type argument from the declaration:
Map<String, List<Integer>> m = new HashMap<String, List<Integer>>(); // pre-7
Map<String, List<Integer>> m = new HashMap<>(); // ✅
var m = new HashMap<String, List<Integer>>(); // Java 10+ (Day 030)
Generic methods
The type parameter belongs to the method, declared before the return type:
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i]; array[i] = array[j]; array[j] = temp;
}
public static <K, V> Map<V, K> invert(Map<K, V> map) { ... }
Type inference usually makes the call site clean:
swap(names, 0, 1); // T inferred as String
Collections.<String>emptyList(); // explicit, rarely needed
A generic method can live in a non-generic class, and its type parameter is independent of any
class-level one. Collections is entirely non-generic with dozens of generic methods.
Bounded type parameters
extends constrains what T can be — and note it means "extends or implements", for both
classes and interfaces:
public <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) total += n.doubleValue(); // ✅ Number's methods are available
return total;
}
Without the bound, T is only known to be Object — you could call toString and nothing else.
Multiple bounds
public <T extends Comparable<T> & Serializable> T max(List<T> list) { ... }
The class bound must come first, and there can be at most one class among the bounds — single inheritance again (Day 037).
Recursive bounds — the pattern that looks strange
public static <T extends Comparable<T>> T max(List<T> list) { ... }
Read it as: "T must be comparable to itself." Without it, T extends Comparable would allow a
List<Dog> where Dog implements Comparable<Cat> — comparable to the wrong thing.
This is why Enum is declared Enum<E extends Enum<E>> — it looks circular and isn't. It's
saying "the type parameter is the concrete enum type itself", which is what makes compareTo and
getDeclaringClass type-safe.
Being able to explain that self-referential bound is a genuine differentiator — most people find it baffling.
Generic classes vs generic methods
| Use | |
|---|---|
| Generic class | The type is part of the object's identity — List<String>, Box<T> |
| Generic method | The type is per-invocation — Collections.sort, Arrays.asList |
Prefer a generic method when the type doesn't need to persist. A generic class forces every user to pick a type argument even when it's irrelevant to them.
Generics are invariant — the important rule
List<String> strings = new ArrayList<>();
List<Object> objects = strings; // ❌ COMPILE ERROR
List<String> is NOT a subtype of List<Object>, even though String is a subtype of Object.
Why — this is the whole reason, and it's Day 031's array problem fixed:
// If it were allowed:
List<Object> objects = strings; // hypothetically
objects.add(42); // legal for List<Object>
String s = strings.get(0); // 💥 boom — an Integer in a List<String>
Compare with arrays, which ARE covariant and therefore unsound:
Object[] objects = new String[3]; // ✅ compiles — arrays are covariant
objects[0] = 42; // 💥 ArrayStoreException at RUNTIME
| Arrays | Generics | |
|---|---|---|
| Variance | Covariant | Invariant |
| Type errors caught | Runtime (ArrayStoreException) |
Compile time |
| Type info at runtime | Retained (reified) | Erased (Day 055) |
Arrays and generics have opposite properties, which is exactly why they mix badly — Day 055 covers that.
Invariance is safe but restrictive, which is what wildcards exist to relax (Day 056).
Where generics can't go
class Box<T> {
private T item; // ✅
private static T shared; // ❌ static — T isn't known per-class
private T[] array = new T[10]; // ❌ cannot instantiate a generic array
void method() {
if (item instanceof T) { } // ❌ erased at runtime (Day 055)
T t = new T(); // ❌ cannot instantiate T
}
}
class MyException<T> extends Exception { } // ❌ cannot be generic and Throwable
List<int> nums; // ❌ primitives (Day 028) — use List<Integer>
Every one of these restrictions traces to erasure, which is tomorrow.
The generic array workaround:
@SuppressWarnings("unchecked")
private T[] array = (T[]) new Object[10]; // what ArrayList actually does
That's literally ArrayList's elementData (Day 046) — an Object[] cast on access. Which is
why ArrayList.toArray() needs you to pass an array to get a correctly-typed result.
Type this yourself
import java.util.*;
import java.io.Serializable;
public class GenericsDemo {
// ---- generic class ----
static class Box<T> {
private T content;
void set(T c) { content = c; }
T get() { return content; }
<U> Pair<T, U> pairWith(U other) { return new Pair<>(content, other); } // method-level U
}
record Pair<A, B>(A first, B second) { }
// ---- generic methods ----
static <T> void swap(T[] a, int i, int j) { T t = a[i]; a[i] = a[j]; a[j] = t; }
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T n : list) total += n.doubleValue();
return total;
}
// recursive bound: T must be comparable to ITSELF
static <T extends Comparable<T>> T max(List<T> list) {
T best = list.get(0);
for (T t : list) if (t.compareTo(best) > 0) best = t;
return best;
}
// multiple bounds — class first, then interfaces
static <T extends Number & Comparable<T> & Serializable> T maxNumber(List<T> list) {
return max(list);
}
// a typed generic "array" the way ArrayList does it
static class SimpleList<T> {
@SuppressWarnings("unchecked")
private T[] data = (T[]) new Object[10];
private int size;
void add(T item) { data[size++] = item; }
T get(int i) { return data[i]; }
}
public static void main(String[] args) {
System.out.println("--- generic class ---");
Box<String> box = new Box<>();
box.set("hello");
System.out.println(" get() = " + box.get() + " (no cast needed)");
System.out.println(" pairWith(42) = " + box.pairWith(42));
System.out.println("\n--- generic methods ---");
String[] names = {"a", "b", "c"};
swap(names, 0, 2);
System.out.println(" after swap: " + Arrays.toString(names));
System.out.println(" sum([1, 2.5, 3L]) = " + sum(List.of(1, 2.5, 3L)));
System.out.println(" max([3,7,2]) = " + max(List.of(3, 7, 2)));
System.out.println(" max(['b','x','a'])= " + max(List.of("b", "x", "a")));
System.out.println("\n--- invariance ---");
List<String> strings = new ArrayList<>(List.of("a"));
// List<Object> objects = strings; // ← uncomment: compile error
System.out.println(" List<String> is NOT a List<Object> — compile error if you try");
System.out.println("\n--- arrays ARE covariant, and unsound ---");
Object[] arr = new String[2];
try { arr[0] = 42; }
catch (ArrayStoreException e) {
System.out.println(" Object[] o = new String[2]; o[0] = 42; → ArrayStoreException");
System.out.println(" ↑ a RUNTIME error. Generics make the same mistake a COMPILE error.");
}
System.out.println("\n--- the ArrayList trick ---");
SimpleList<String> sl = new SimpleList<>();
sl.add("x"); sl.add("y");
System.out.println(" " + sl.get(0) + sl.get(1) + " (Object[] cast to T[] internally)");
System.out.println("\n--- self-referential bound ---");
System.out.println(" Enum is declared: Enum<E extends Enum<E>>");
System.out.println(" → 'the type parameter IS the concrete enum type'");
System.out.println(" Day.MON.compareTo(Day.TUE) = " + Day.MON.compareTo(Day.TUE)
+ " ← type-safe because of that bound");
}
enum Day { MON, TUE }
}
The key experiment: uncomment List<Object> objects = strings; and read the compile error. Then
compare with the ArrayStoreException a few lines below — same logical mistake, one caught by the
compiler and one at runtime. That contrast is the argument for generics in a single screen.
Common mistakes
| Mistake | Correction |
|---|---|
Expecting List<String> to be a List<Object> |
Generics are invariant. Use wildcards (Day 056). |
Using raw types (List with no argument) |
Loses all checking and generates unchecked warnings. |
new T() or new T[10] |
Impossible — T is erased. Pass a factory or Class<T>. |
static T field |
A static member is per-class; T is per-instance. |
T extends Comparable without the recursive bound |
Allows comparison to the wrong type. |
| Making a class generic when a method would do | Forces a type argument on every user. |
List<int> |
Primitives can't be type arguments (Day 028). |
Interview questions
Q: Why do generics exist?
To move type errors from runtime to compile time and eliminate casts. Before Java 5 a collection held
Object, so every retrieval needed a cast that could fail at runtime withClassCastException. Generics let the compiler verify element types at the point of insertion.
Q: Why are generics invariant when arrays are covariant?
Array covariance is unsound: you can assign a
String[]to anObject[]and try to store anInteger, so the JVM has to check every store and throwArrayStoreExceptionat runtime. Generics chose invariance so the same mistake is a compile error —List<String>simply isn't aList<Object>. Wildcards then reintroduce controlled variance where it's safe.
Q: What is a bounded type parameter?
A constraint using
extends, which covers both classes and interfaces.T extends Numbermeans the compiler knowsT's members and lets you calldoubleValue. Without a bound,Tis only known to beObject. Multiple bounds are joined with&, and any class bound must come first.
Q: What does <T extends Comparable<T>> mean, and why the repetition?
It's a recursive bound meaning "T must be comparable to itself". Without it,
T extends Comparablewould permit a type comparable to something else entirely. It's the same pattern asEnum<E extends Enum<E>>, which is how the enum's own type flows intocompareToandgetDeclaringClasstype-safely.
Q: Why can't you write new T[10]?
Because
Tis erased at runtime, so the JVM doesn't know what array type to allocate. The standard workaround, whichArrayListitself uses, is to allocate anObject[]and cast it toT[]with a suppressed unchecked warning.
Mini task
- Run
GenericsDemo. Uncomment the invariance line and read the error. - Write a generic
Stack<T>withpush,popandpeek, backed by anObject[]. - Write
<K,V> Map<V,K> invert(Map<K,V>)and use it. - Write a method with three bounds and explain the ordering rule.
- Try
class MyException<T> extends Exception. Read the error and work out why.
Exit questions
- What two problems do generics solve?
- Name the conventional type-parameter letters.
- How do you declare a generic method, and when is it better than a generic class?
- What does
extendsmean in a bound, and what's the ordering rule for multiple bounds? - Explain
<T extends Comparable<T>>andEnum<E extends Enum<E>>. - Why are generics invariant? Show the unsafe code it prevents.
- Contrast arrays and generics on variance, error timing and runtime type info.
- List four things you can't do with a type parameter, and the one reason for all of them.
Articulation drill
Two minutes: "Why can't you assign a List<String> to a List<Object>, when you can assign a
String[] to an Object[]?"
Show the unsafe insertion in both, then note that one fails at compile time and one at runtime.
Previous: Day 053 · Tomorrow: Day 055 — type erasure, and what it costs