Generics
IntermediateWrite type-safe reusable classes and methods with generics — eliminate casts, catch type errors at compile time, and understand type erasure.
Overview
Generics let you write a single class or method that works correctly for any type, with the type checked at compile time rather than causing ClassCastException at runtime. Before generics (Java 1.4), collections stored Object and every retrieval needed a cast. Generics eliminate that overhead and communicate intent clearly. Under the hood, type parameters are erased at compile time (type erasure) — the bytecode uses Object — but the compiler inserts casts and enforces safety so you never need to write them yourself.
Generic Classes & Methods
Declare a type parameter in angle brackets after the class or method name. By convention: T = Type, E = Element, K = Key, V = Value, N = Number, R = Return type. A generic class stores a type parameter as part of its definition; a generic method declares its own type parameter regardless of whether the class is generic.
// Generic class — works for any type T
public class Pair<T, U> {
private final T first;
private final U second;
public Pair(T first, U second) {
this.first = first;
this.second = second;
}
public T getFirst() { return first; }
public U getSecond() { return second; }
@Override public String toString() {
return "(" + first + ", " + second + ")";
}
// Generic method — declares its own <V> independent of class T, U
public static <V> Pair<V, V> of(V value) {
return new Pair<>(value, value);
}
}
// Generic interface
interface Repository<T, ID> {
T findById(ID id);
void save(T entity);
}
public class GenericsDemo {
// Standalone generic method
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
public static void main(String[] args) {
Pair<String, Integer> person = new Pair<>("Alice", 30);
System.out.println(person); // (Alice, 30)
System.out.println(person.getFirst().toUpperCase()); // ALICE — no cast!
Pair<Double, Double> coords = Pair.of(3.14);
System.out.println(coords); // (3.14, 3.14)
System.out.println(max(10, 20)); // 20
System.out.println(max("apple", "banana")); // banana
}
}Bounded Type Parameters & Wildcards
Bounded type parameters restrict what types can be used: <T extends Number> — T must be Number or a subclass (upper bound) <T extends Comparable<T>> — T must implement Comparable
Wildcards (?) represent an unknown type in method signatures: ? extends Type — upper-bounded wildcard (read from it) ? super Type — lower-bounded wildcard (write to it) ? — unbounded wildcard (only use Object methods)
PECS rule (Producer Extends, Consumer Super): if a collection produces values you read, use extends; if it consumes values you write, use super.
import java.util.List;
import java.util.ArrayList;
public class BoundedDemo {
// Bounded type parameter — T must be a Number
static <T extends Number> double sum(List<T> list) {
double total = 0;
for (T item : list) total += item.doubleValue(); // Number API available
return total;
}
// Upper-bounded wildcard — read from a list of Number or any subtype
// PRODUCER (we read values) → use extends
static double sumWild(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
// Lower-bounded wildcard — write Numbers into a list of Number or supertype
// CONSUMER (we write values) → use super
static void addNumbers(List<? super Integer> list) {
list.add(1); list.add(2); list.add(3);
}
public static void main(String[] args) {
List<Integer> ints = List.of(1, 2, 3);
List<Double> doubles = List.of(1.5, 2.5, 3.5);
System.out.println(sum(ints)); // 6.0
System.out.println(sum(doubles)); // 7.5
System.out.println(sumWild(ints)); // 6.0 — wildcard version
System.out.println(sumWild(doubles));// 7.5
// Lower-bounded: List<Integer>, List<Number>, List<Object> all work
List<Number> numbers = new ArrayList<>();
addNumbers(numbers);
System.out.println(numbers); // [1, 2, 3]
// ? extends: cannot add (type unknown), can read as Number
List<? extends Number> readOnly = ints;
// readOnly.add(4); // compile error — unknown subtype
Number n = readOnly.get(0); // OK — know it is at least a Number
}
}Type Erasure & Practical Implications
At compile time the compiler replaces type parameters with Object (or the bound, if bounded) and inserts casts where needed. At runtime, List<String> and List<Integer> are both just List — this is type erasure. Implications:
• Cannot use instanceof with a generic type: list instanceof List<String> is a compile error • Cannot create generic arrays: new T[10] is illegal • Cannot overload methods that differ only in type parameter: method(List<String>) and method(List<Integer>) have the same erasure • Static fields cannot use the class type parameter — they are shared across all parameterisations
import java.util.ArrayList;
import java.util.List;
public class ErasureDemo {
// This WORKS — bounded erasure replaces T with Comparable
static <T extends Comparable<T>> T clamp(T val, T min, T max) {
if (val.compareTo(min) < 0) return min;
if (val.compareTo(max) > 0) return max;
return val;
}
// Generic stack — array workaround for type erasure
@SuppressWarnings("unchecked")
static class Stack<E> {
private Object[] elements; // can't do new E[10] — use Object[]
private int size = 0;
Stack(int capacity) { elements = new Object[capacity]; }
void push(E item) { elements[size++] = item; }
E pop() {
if (size == 0) throw new java.util.EmptyStackException();
return (E) elements[--size]; // safe unchecked cast
}
}
public static void main(String[] args) {
System.out.println(clamp(15, 0, 10)); // 10
System.out.println(clamp(5, 0, 10)); // 5
Stack<String> stack = new Stack<>(10);
stack.push("first");
stack.push("second");
System.out.println(stack.pop()); // second
// instanceof with raw type is OK; parameterised type is not
List<String> list = new ArrayList<>();
System.out.println(list instanceof List); // true — OK
// System.out.println(list instanceof List<String>); // compile error
// Both erased to List at runtime — can't overload on type parameter
// void process(List<String> s) { }
// void process(List<Integer> i) { } // compile error: same erasure
}
}Key Points to Remember
- Generics catch type errors at compile time — no ClassCastException, no manual casts
- PECS: Producer Extends (read), Consumer Super (write) — guides wildcard choice
- Type erasure: <T> becomes Object at runtime; List<String> and List<Integer> are the same class
- Cannot do instanceof with parameterised types, or create generic arrays (new T[])
- Bounded wildcard <? extends Number> allows reading; <? super Integer> allows writing
- Static fields cannot use the class-level type parameter — they are shared across all instances
Practice Generics in the Playground
Run and modify code directly in your browser - no setup needed.
Interview Questions
Sign in to ask AriaWhat is type erasure in Java generics?
What is the PECS principle? Give an example.
Why can't you create a generic array like new T[10] in Java?
What is the difference between List<?>, List<Object>, and List<T>?
Can you overload two methods that differ only in their generic type parameter?
Ask Aria about Generics
Your personal AI tutor — ask anything about this concept