Write type-safe, reusable code with Java generics.
Published September 21, 2026
Before Java 5, a List held plain Objects. You could put anything in, and you had to cast everything you took out. Put an Integer into a list meant for Strings and nothing complained until a ClassCastException crashed the program at runtime, far from the actual mistake.
Generics let classes and methods take type parameters, so the compiler knows what a container holds and catches type mistakes at compile time:
List raw = new ArrayList(); // pre-generics
raw.add("hello");
raw.add(42); // accepted
String s = (String) raw.get(1); // ๐ฅ ClassCastException at runtime
List<String> names = new ArrayList<>();
names.add(42); // โ compile error โ caught immediately
String first = names.get(0); // no cast needed
You get both type safety and reuse: one ArrayList implementation works for any element type.
public class Box<T> { // T is a type parameter, a placeholder
private final T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> b1 = new Box<>("hi"); // the "diamond" <> infers <String>
Box<Integer> b2 = new Box<>(7);
String s = b1.get(); // typed: no cast
A class can have several parameters, for example Map<K, V> or Pair<A, B>. By convention, parameter names are single capitals: T (type), E (element), K/V (key/value), R (result).
A method can declare its own type parameter, placed before the return type:
public static <T> List<T> repeat(T item, int times) {
List<T> out = new ArrayList<>();
for (int i = 0; i < times; i++) out.add(item);
return out;
}
List<String> xs = repeat("a", 3); // T inferred as String
Sometimes the code needs to call methods on T, so T must be restricted to types that have them:
// T must be comparable to itself, so compareTo is available
public 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;
return best;
}
extends in a bound means "is a subtype of", for classes and interfaces. Several bounds can be combined: <T extends Number & Comparable<T>>.
List<Integer> is not a List<Number>This surprises almost everyone at first. Integer is a subtype of Number, but List<Integer> is not a subtype of List<Number>. Generic types are invariant. If it were allowed, this would compile:
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // โ not allowed โ and here's why:
nums.add(3.14); // a Double inside a list of Integers
Integer i = ints.get(0); // ๐ฅ would crash
(Arrays are covariant, so Integer[] is a Number[], and the equivalent mistake compiles and fails at runtime with ArrayStoreException. Generics were designed to prevent exactly that.)
To write a method that accepts lists of different element types, use wildcards:
// Reads Numbers from a list of any Number subtype
double sum(List<? extends Number> list) {
double total = 0;
for (Number n : list) total += n.doubleValue(); // โ
reading as Number is safe
// list.add(1); // โ can't add: the real type might be List<Double>
return total;
}
sum(List.of(1, 2, 3)); // List<Integer> โ
sum(List.of(1.5, 2.5)); // List<Double> โ
// Writes Integers into a list that can hold them
void fillWithZeros(List<? super Integer> list, int n) {
for (int i = 0; i < n; i++) list.add(0); // โ
an Integer fits in List<Integer>, List<Number>, List<Object>
// Integer x = list.get(0); // โ we only know it's some supertype: reading gives Object
}
extends, Consumer super? extends T.? super T.T.The JDK follows this everywhere, for example Collections.copy(List<? super T> dest, List<? extends T> src). A plain ? (List<?>) means "a list of some unknown type": you can read elements as Object and check its size, but you can't add anything except null.
The compiler uses type parameters to check your code, then erases them. At runtime, List<String> and List<Integer> are both just List, and the compiler inserts casts where needed. This kept generic code compatible with pre-Java-5 libraries, but it has consequences:
new T(); // โ the type isn't known at runtime
new T[10]; // โ can't create a generic array
obj instanceof List<String> // โ can't test a parameterized type (List<?> is fine)
static T field; // โ a static field is shared by all Box<X>, so it can't use T
void f(List<String> a); void f(List<Integer> a); // โ both erase to f(List): a clash
The usual workaround when code needs the actual type is to pass a Class<T> (a "type token"): <T> T parse(String json, Class<T> type). That's exactly how Jackson and Spring's getBean(Class<T>) work. For nested generic types (a List<Order>), libraries use a subclassing trick such as Jackson's TypeReference<List<Order>>.
Also, generics work only with reference types: List<int> isn't allowed, so it's List<Integer>, with autoboxing. In performance-critical numeric code, that boxing costs memory and time. Primitive streams (IntStream) and specialized collections avoid it.
Using a generic class without type arguments (List list) creates a raw type, which turns off generic checking for that variable. It exists only for legacy compatibility. Treat "unchecked" compiler warnings seriously. Suppress them with @SuppressWarnings("unchecked") only on the smallest possible scope, and only when you've verified the cast is safe.
Q: Why can't you add an Integer to a List<? extends Number>?
A: The wildcard means "a list of some specific subtype of Number, unknown here". It might be a List<Double>, and adding an Integer to it would corrupt it. So the compiler forbids adding anything except null. Reading is safe, because every element is at least a Number.
Q: What is PECS?
A: "Producer Extends, Consumer Super". Use ? extends T for a parameter you only read from (it produces Ts), ? super T for one you only write to (it consumes Ts), and exact T when you do both. It makes APIs accept the widest range of argument types safely.
Q: What is type erasure, and what can't you do because of it?
A: The compiler checks generic types and then removes them, so the runtime sees raw types plus inserted casts. Because of that you can't create new T() or new T[], can't use instanceof with a parameterized type, can't overload methods whose parameters differ only in type arguments, and can't use type parameters in static members.
Q: Why is List<Integer> not a subtype of List<Number>?
A: If it were, you could add a Double through the List<Number> view into a list that's really List<Integer>, and the program would fail later. Generic invariance prevents this at compile time. Wildcards (List<? extends Number>) provide safe, read-only flexibility instead.
Q: How do you get the runtime class of T inside a generic class?
A: You can't from T itself. Pass a Class<T> into the constructor or method (a type token), or, for complex generic types, capture it with a subclass of a type-reference helper, as Jackson's TypeReference or Spring's ParameterizedTypeReference do.