What Are Generics?
Generics let you write classes, interfaces, and methods that
work with any type while keeping full compile-time type safety. Instead
of writing separate StringBox, IntegerBox, and
UserBox classes, you write one Box<T> and the
compiler generates a type-safe version for each use.
The problem they solve: before Java 5, collections stored raw
Object references. You could put anything in a list — then crash
at runtime when you retrieved it and the cast failed. Generics move that error
to compile time, where it belongs.
// Before generics (Java 1.4): everything is Object — no safety
List names = new ArrayList();
names.add("Alice");
names.add(42); // compiles fine — disaster waiting to happen
String s = (String) names.get(1); // ClassCastException at runtime!
// With generics (Java 5+): the compiler is your safety net
List<String> names = new ArrayList<>();
names.add("Alice");
names.add(42); // compile error: int is not a String
String s = names.get(0); // no cast needed — compiler guarantees it's a String
By convention: T — general Type, E — Element
(collections), K/V — Key/Value (maps),
N — Number, R — Return type (functions).
These are just conventions — the compiler accepts any identifier.
Generic Classes
Declare type parameters in angle brackets after the class name. Every use of
T in the class is replaced by the actual type at the call site.
// A reusable container for any type — one class, infinite uses
public class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
public boolean isEmpty() { return content == null; }
}
Box<String> stringBox = new Box<>();
stringBox.set("hello");
String s = stringBox.get(); // no cast — compiler knows it's String
Box<Integer> intBox = new Box<>();
intBox.set(42);
int n = intBox.get(); // auto-unboxing
Multiple type parameters
// Pair<K,V> — the same pattern Map uses internally
public class Pair<K, V> {
private final K key;
private final V value;
public Pair(K key, V value) { this.key = key; this.value = value; }
public K key() { return key; }
public V value() { return value; }
// Static factory — cleaner than constructor with two same-type args
public static <K, V> Pair<K, V> of(K k, V v) { return new Pair<>(k, v); }
}
Pair<String, Integer> nameAge = Pair.of("Alice", 30);
Pair<Long, Boolean> idActive = Pair.of(1001L, true);
Generic interfaces — the Repository pattern
// One interface, multiple entity types — no code duplication
public interface Repository<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void delete(ID id);
}
// Implementation binds the type parameters to concrete types
public class UserRepository implements Repository<User, Long> {
@Override
public Optional<User> findById(Long id) { return db.findUser(id); }
// ... other methods
}
public class OrderRepository implements Repository<Order, Long> { ... }
// Spring Data JPA is exactly this pattern at scale:
// public interface UserRepository extends JpaRepository<User, Long> {}
Generic Methods
Methods can declare their own type parameters, independent of any class-level
parameter. The <T> goes before the return type.
public class Collections2 {
// T declared before return type — method-level type parameter
public static <T> Optional<T> first(List<T> list) {
return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0));
}
// Two type parameters — converts between types
public static <T, R> List<R> transform(List<T> list, Function<T, R> fn) {
return list.stream().map(fn).collect(Collectors.toList());
}
// Bounded: T must be Comparable — gives access to compareTo()
public static <T extends Comparable<T>> T max(List<T> list) {
return list.stream().max(Comparator.naturalOrder()).orElseThrow();
}
}
// Type inference — compiler figures out T from the argument
Optional<String> first = Collections2.first(List.of("a", "b"));
List<Integer> lengths = Collections2.transform(
List.of("Alice", "Bob"),
String::length // T=String, R=Integer — inferred automatically
);
Integer biggest = Collections2.max(List.of(3, 1, 4, 1, 5)); // 5
Bounded Type Parameters
Bounds restrict which types can be used as type arguments AND give you access to the bound's methods inside the generic code.
// Upper bound: T must be Number or a subtype
// Unlocks Number's methods inside the class
public class NumericBox<T extends Number> {
private final T value;
public NumericBox(T value) { this.value = value; }
public double doubled() { return value.doubleValue() * 2; } // Number method
public boolean isPositive() { return value.doubleValue() > 0; }
}
NumericBox<Integer> intBox = new NumericBox<>(5); // ✅ Integer extends Number
NumericBox<BigDecimal> bigBox = new NumericBox<>(new BigDecimal("9.99")); // ✅
// NumericBox<String> bad = new NumericBox<>("x"); // ❌ compile error
// Multiple bounds: class first, then interfaces, separated by &
public static <T extends Number & Comparable<T>> T clamp(T value, T min, T max) {
if (value.compareTo(min) < 0) return min;
if (value.compareTo(max) > 0) return max;
return value;
}
Integer result = clamp(150, 0, 100); // 100 — Integer satisfies both bounds
Wildcards and PECS
Wildcards (?) appear in method parameters to accept
collections of related types. They solve a key problem: List<Integer>
is NOT a subtype of List<Number> even though
Integer extends Number — generics are invariant.
// The invariance problem:
List<Integer> ints = List.of(1, 2, 3);
// sumList(ints); ← compile error if sumList takes List<Number>
// Fix: wildcard accepts List<Integer>, List<Double>, List<Number>…
public static double sumList(List<? extends Number> list) {
return list.stream().mapToDouble(Number::doubleValue).sum();
}
sumList(ints); // ✅ works
sumList(List.of(1.5, 2.5)); // ✅ works — List<Double>
PECS — Producer Extends, Consumer Super
This is the single rule that governs wildcard choice:
/*
* ? extends T → you READ from the collection (it PRODUCES values)
* ? super T → you WRITE to the collection (it CONSUMES values)
* T (no wildcard) → you do BOTH
*
* The canonical example: Collections.copy(dest, src)
*/
public static <T> void copy(
List<? super T> dest, // CONSUMER — we add T into dest
List<? extends T> src) { // PRODUCER — we read T from src
for (T item : src) dest.add(item);
}
List<Number> dest = new ArrayList<>();
List<Integer> src = List.of(1, 2, 3);
copy(dest, src); // ✅
// Why you CANNOT add to ? extends:
public void cannotAdd(List<? extends Number> list) {
// list.add(1); ❌ — compiler doesn't know if it's List<Integer>, List<Double>…
// list.add(1.0); ❌ — same reason: unsafe
Number n = list.get(0); // ✅ reading is always safe
}
// Why you CANNOT read typed value from ? super:
public void cannotRead(List<? super Integer> list) {
list.add(42); // ✅ writing Integer is safe
Object o = list.get(0); // ✅ but you only get Object back
// Integer i = list.get(0); ❌ — could be List<Number> or List<Object>
}
| Wildcard | Read? | Write? | Use when |
|---|---|---|---|
List<T> |
✅ as T | ✅ T | Both read and write |
List<? extends T> |
✅ as T | ❌ | Read-only (PRODUCER) |
List<? super T> |
✅ as Object | ✅ T | Write-only (CONSUMER) |
List<?> |
✅ as Object | ❌ | Unknown type, read as Object |
Type Erasure — What the Compiler Actually Does
Java generics are implemented entirely at compile time via type erasure. The compiler checks your generic types, then strips them — the bytecode contains no generic information. This was a deliberate design choice for backward compatibility with pre-Java 5 bytecode.
/*
* YOUR CODE AFTER ERASURE (bytecode)
* ───────────────────── ──────────────────────────
* List<String> list = ... List list = ...
* String s = list.get(0); String s = (String) list.get(0);
*
* Box<Integer> box = ... Box box = ...
* Integer i = box.get(); Integer i = (Integer) box.get();
*
* <T extends Number> T max(...) Number max(...) ← bound becomes the erased type
* <T> T first(...) Object first(...) ← no bound → Object
*/
// Consequence 1: List<String> and List<Integer> are the SAME class at runtime
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
strings.getClass() == integers.getClass(); // true — both are ArrayList
// Consequence 2: instanceof with parameterized types is illegal
// if (obj instanceof List<String>) {} // compile error
if (obj instanceof List<?>) {} // ✅ wildcard works
// Consequence 3: cannot create instances or arrays of type parameters
// new T(); ❌ — JVM doesn't know what T is
// new T[10]; ❌ — arrays check types at runtime, generics don't
// Workaround for new T(): pass Class<T> and use reflection
public static <T> T newInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
// Or better: pass a Supplier<T> factory
public static <T> T create(Supplier<T> factory) { return factory.get(); }
User u = create(User::new);
Common Pitfalls
// ❌ Raw type: disables ALL type checking for this variable
List raw = new ArrayList();
raw.add("string");
raw.add(42); // no compile error — disaster at runtime
// ✅ Always parameterize
List<String> typed = new ArrayList<>();
// typed.add(42); compile error: caught before shipping
// ❌ Heap pollution: a variable of parameterized type holds wrong-typed data
// Happens when you mix raw types and generics with unchecked casts
List<String> strings = new ArrayList<>();
List raw = strings; // raw alias — no warning here
raw.add(42); // ← heap pollution: Integer in a List<String>
String s = strings.get(0); // ClassCastException at runtime — far from the cause
// Lesson: never assign a raw type alias to a parameterized variable.
// Enable -Xlint:unchecked in your build to catch all raw type warnings.
// ❌ Cannot create generic arrays — arrays are covariant, generics are invariant
// List<String>[] arr = new ArrayList<String>[10]; // compile error
// ✅ Use List of Lists instead
List<List<String>> lists = new ArrayList<>();
Interview Questions
Q: What problem do generics solve?
Before Java 5, collections stored raw Object references —
you could add anything, then crash with ClassCastException
when retrieving. Generics move that error to compile time: the compiler
rejects type mismatches before the code runs. They also eliminate the
need for explicit casts when retrieving elements.
Q: What is the diamond operator <>?
Introduced in Java 7, it tells the compiler to infer the type arguments
from context: new ArrayList<>() instead of
new ArrayList<String>(). The type is inferred from
the variable declaration on the left. Purely a convenience — no runtime
effect.
Q: What is PECS?
Producer Extends, Consumer Super. When a collection produces
values you read, use ? extends T. When a collection
consumes values you write, use ? super T.
Example: Collections.copy(List<? super T> dest,
List<? extends T> src).
Q: What is type erasure and what are its practical consequences?
The compiler checks generic types then removes them from the bytecode —
replacing type parameters with their bounds (Object if unbounded,
the bound class otherwise) and inserting casts where needed. Consequences:
(1) List<String> and List<Integer> are
the same class at runtime; (2) you cannot use instanceof with
parameterised types; (3) you cannot create new T() or
new T[]; (4) you cannot overload methods that differ only in
generic type parameters — they erase to the same signature.
Q: Why are List<Integer> and
List<Number> unrelated types, even though Integer extends Number?
Generics are invariant by design. If List<Integer>
were a subtype of List<Number>, you could write:
List<Number> nums = intList; nums.add(3.14); — putting a
Double into a List<Integer>. That would break
type safety silently. Arrays are covariant (and this exact bug is possible with
arrays — that's why ArrayStoreException exists). Generics chose
invariance to prevent it. Wildcards (? extends) restore flexibility
when needed, with compile-time read/write restrictions to keep it safe.
Q: How do you instantiate a generic type T at runtime?
You can't use new T() because of type erasure — the JVM doesn't
know what T is. Two clean solutions: (1) pass a Class<T>
token and use clazz.getDeclaredConstructor().newInstance() —
reflection-based, throws checked exceptions; (2) pass a
Supplier<T> factory — type-safe, no reflection, no checked
exceptions, preferred in modern code. Spring uses Class tokens extensively
(e.g. RestTemplate.getForObject(url, User.class)).
Q: What is <T extends Comparable<? super T>>?
This is the bound used by Collections.sort(). The ? super T
means T can be compared using a Comparable defined on T itself
or on any supertype. Without the ? super, a class like
LocalDate that implements ChronoLocalDate
(which extends Comparable<ChronoLocalDate>, not
Comparable<LocalDate>) wouldn't satisfy the bound.
The ? super makes the bound flexible enough to cover this.