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

Common mistakes

  • Using raw types β€” defeats the whole point. Always parameterise.
  • Writing List<Object> when you mean List<?> β€” the first is not a supertype of List<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.