Topic 9. Generics
Generics provide type-parameterized code: you write reusable logic while preserving compile-time type safety.
They solve two practical problems:
- reduce unsafe runtime casts,
- make APIs self-descriptive through type contracts.
Why generics matter
Without generics (raw types), type errors appear late:
List list = new ArrayList();
list.add("Alice");
list.add(100);
String name = (String) list.get(1); // ClassCastException at runtime
With generics, compiler catches wrong usage:
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(100); // compile-time error
String first = names.get(0);
Basic syntax
T,E,K,Vare conventional type parameter names.List<String>means this list stores only strings.Map<String, Integer>means string keys and integer values.
Common conventions:
T= Type,E= Element,K/V= Key/Value,R= Result type.
Example 1. Generic method
public static <T> void printAll(List<T> items) {
for (T item : items) {
System.out.println(item);
}
}
Usage:
printAll(List.of("A", "B", "C"));
printAll(List.of(1, 2, 3));
Example 2. Generic class
public class Box<T> {
private T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
Usage:
Box<String> textBox = new Box<>();
textBox.set("hello");
String text = textBox.get();
Box<Integer> intBox = new Box<>();
intBox.set(42);
int number = intBox.get();
Example 3. extends and super wildcards
public static double sum(List<? extends Number> nums) {
double total = 0.0;
for (Number n : nums) {
total += n.doubleValue();
}
return total;
}
public static void addDefaults(List<? super Integer> out) {
out.add(10);
out.add(20);
}
Meaning:
? extends Number: safe read asNumber, restricted writes.? super Integer: safe write ofInteger, read asObject.
PECS rule
Producer Extends, Consumer Super:
- If structure produces values for you to read, use
extends. - If structure consumes values you write, use
super.
Invariance in Java generics
List<Integer> is not a subtype of List<Number>.
List<Integer> ints = List.of(1, 2, 3);
// List<Number> nums = ints; // compile error
This prevents unsafe insertion of incompatible values.
Type erasure
Java generics are implemented through erasure:
- type checks happen at compile time,
- runtime mostly does not retain concrete generic arguments.
Practical implications:
- cannot do
new T(), - cannot reliably check
instanceof List<String>, - cannot create
new List<String>[10].
Bounded type parameters
public static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
T must support comparison with same type.
Multiple bounds are possible:
<T extends Number & Comparable<T>>
Generics + collections API design
Poor signature:
void copy(List<Object> dst, List<Object> src)
Better:
public static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T item : src) {
dst.add(item);
}
}
This is reusable and type-safe.
Common beginner mistakes
- using raw types (
List list), - mixing up
extendsandsuper, - forcing unchecked casts,
- overcomplicating type signatures unnecessarily.
Practical checklist
- Always parameterize collections.
- Use
extendsfor read-oriented APIs. - Use
superfor write-oriented APIs. - Keep generic signatures readable.
- Add concise Javadoc when type contracts are non-trivial.
Key takeaway
Generics are a core quality mechanism: fewer runtime failures, clearer contracts, and safer reusable APIs.