Java Generics β Type Parameters, Wildcards, Bounded Types
Generics let you write code that works for any type while keeping the compile-time type check. Instead of returning Object and casting everywhere, List<String> guarantees you can only put strings in and you always get strings out.
Type parameters
// Generic class
public class Box<T> {
private final T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> s = new Box<>("hello"); // diamond β infers <String>
Box<Integer> i = new Box<>(42);
String v = s.get(); // no cast needed
Generic methods
public static <T> List<T> repeat(T value, int n) {
List<T> out = new ArrayList<>();
for (int i = 0; i < n; i++) out.add(value);
return out;
}
var words = repeat("hi", 3); // List<String> inferred
Bounded type parameters
public static <T extends Comparable<T>> T max(List<T> xs) {
T best = xs.get(0);
for (T x : xs) if (x.compareTo(best) > 0) best = x;
return best;
}
// β
max(List.of(1, 2, 3))
// β
max(List.of("a", "b", "c"))
// β max(List.of(new Object(), new Object())) β Object isn't Comparable
Wildcards and PECS β Producer Extends, Consumer Super
// Producer β we READ from it β use extends
void printAll(List<? extends Number> xs) {
for (Number n : xs) System.out.println(n);
}
printAll(List.<Integer>of(1, 2)); // β
printAll(List.<Double>of(1.0, 2.0)); // β
// Consumer β we WRITE to it β use super
void addOnes(List<? super Integer> xs) {
xs.add(1);
}
addOnes(new ArrayList<Number>()); // β
addOnes(new ArrayList<Object>()); // β
Rule of thumb: if you only read, use ? extends T. If you only write, use ? super T. If you do both, use T.
Type erasure β the catch
Generics exist at compile time. At runtime, List<String> and List<Integer> are both just List. Consequences:
List<String> a = new ArrayList<>();
List<Integer> b = new ArrayList<>();
a.getClass() == b.getClass(); // true β both ArrayList
// Can't use primitives β wrappers only
List<int> xs; // β
List<Integer> xs; // β
// Can't create arrays of generic types
T[] arr = new T[10]; // β
T[] arr = (T[]) new Object[10]; // β
but unchecked cast
// Can't use generic type in instanceof
if (x instanceof List<String>) // β
if (x instanceof List<?>) // β
Raw types β the legacy danger
List list = new ArrayList(); // raw β don't do this
list.add("hi");
list.add(1); // compiles, but...
String s = (String) list.get(1); // ClassCastException at runtime
Raw types exist for backward compatibility with pre-Java-5 code. Always use a parameterised type (List<?> if you truly don't know).
Records + generics
public record Pair<A, B>(A first, B second) {}
var p = new Pair<>("Alice", 30); // Pair<String, Integer>
All sub-topics
- Type parameters (
<T>) - Wildcards (
?,? extends,? super) - Bounded type parameters
- Type erasure
- Raw types
Common mistakes
- Using raw types β defeats the whole point. Always parameterise.
- Writing
List<Object>when you meanList<?>β the first is not a supertype ofList<String>; the second is. - Expecting to see generic type at runtime β it's erased. Design accordingly (pass
Class<T>if you need the type). - Over-genericising β one type parameter is usually enough. If you reach for four, reconsider the design.
Try it & related tools
Generics shine with the JSON to POJO tool β nested lists and maps become List<Map<String, Thing>>. Experiment in the Java Online Compiler.