Generics¶
Generics enable type-safe code at compile time without casting. Java uses type erasure — generic type info is removed at runtime. Key concepts: bounded types (<T extends Comparable>), wildcards (? extends T, ? super T — PECS: Producer Extends, Consumer Super), and generic methods.
Type Parameters & Generic Classes¶
Generic classes and methods use type parameters (<T>, <K, V>) to create reusable, type-safe code. The type is specified at usage time, and the compiler enforces it. No casting needed — compiler guarantees the types at compile time.
Deep Dive: Basic Syntax
// Generic class
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
// Usage — type-safe, no casting
Box<Integer> intBox = new Box<>();
intBox.set(10);
Integer value = intBox.get(); // No cast needed
// Generic method — type parameter scoped to the method
public static <T> T getFirst(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
String first = getFirst(strings); // T inferred as String
Type Erasure¶
Java generics are implemented via type erasure — the compiler removes all generic type info at runtime, replacing with casts. This ensures backward compatibility with pre-generics code (Java < 5). Consequence: you can't create generic arrays (new T[10]), use primitives (List<int>), or get type at runtime (T.class).
Deep Dive: What Erasure Does
Before compilation:
After type erasure (bytecode equivalent):
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // Compiler inserts cast
Proof of erasure:
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true!
// Both are just 'ArrayList' at runtime
What you CAN'T do because of erasure:
new T[10]— cannot create generic arraysList<int>— cannot use primitives (useList<Integer>)T.class— cannot get type at runtimeinstanceof List<String>— cannot check generic type at runtime
Bounded Type Parameters¶
Upper bound (<T extends X>) restricts T to X or its subclasses — lets you call X's methods. Multiple bounds (<T extends Comparable<T> & Serializable>) require T to satisfy all bounds. Class bound must come first, followed by interfaces.
Deep Dive: Examples
// Upper bound — T must be Number or subclass
public class NumberBox<T extends Number> {
private T value;
public double doubleValue() {
return value.doubleValue(); // Can call Number methods!
}
}
NumberBox<Integer> intBox = new NumberBox<>(); // OK
NumberBox<Double> dblBox = new NumberBox<>(); // OK
NumberBox<String> strBox = new NumberBox<>(); // Compilation error!
// Multiple bounds — T must implement both
public <T extends Comparable<T> & Serializable> void sort(List<T> list) {
Collections.sort(list);
}
Wildcards & PECS¶
PECS: Producer Extends, Consumer Super. Use <? extends T> when reading from a structure (it produces T values). Use <? super T> when writing to a structure (it consumes T values). Use <T> when doing both. <?> is unbounded — read as Object, can't add anything (except null).
Deep Dive: Wildcard Types with Examples
Upper bounded — <? extends T> (producer — read only):
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number num : numbers) {
total += num.doubleValue(); // Can read as Number
}
// numbers.add(42); // ❌ Cannot add — compiler doesn't know exact type
return total;
}
sum(Arrays.asList(1, 2, 3)); // List<Integer> works
sum(Arrays.asList(1.5, 2.5)); // List<Double> works
Lower bounded — <? super T> (consumer — write only):
public void addIntegers(List<? super Integer> list) {
list.add(1); // Can add Integer
list.add(2); // Can add Integer
// Integer x = list.get(0); // ❌ Can only read as Object
}
addIntegers(new ArrayList<Number>()); // Works
addIntegers(new ArrayList<Object>()); // Works
PECS in action — copy() method:
public static <T> void copy(List<? extends T> source, List<? super T> dest) {
for (T item : source) { // source is a producer (extends)
dest.add(item); // dest is a consumer (super)
}
}
List<Integer> ints = Arrays.asList(1, 2, 3);
List<Number> numbers = new ArrayList<>();
copy(ints, numbers); // Works!
Common Generics Pitfalls¶
Due to type erasure, you cannot: create generic arrays, use primitives as type parameters, use instanceof with generics, or access T.class. Static fields can't use class-level type parameters (they're shared across all instances). These are common interview trick questions.
Deep Dive: Pitfall Examples
// Cannot instantiate generic types
public class Box<T> {
private T[] array;
public Box(int size) {
array = new T[size]; // ❌ Compilation error
array = (T[]) new Object[size]; // ⚠️ Unchecked cast warning
}
}
// Cannot use static fields with type parameters
public class Box<T> {
private static T value; // ❌ Error: static can't use class type param
}
// Cannot catch generic exceptions
public <T extends Exception> void method() {
try { }
catch (T e) { } // ❌ Cannot catch type parameter
}
Common Interview Questions¶
Common Interview Questions
- What are generics? Why were they introduced?
- What is type erasure?
- Can you create a generic array in Java?
- What is the difference between
<T>,<?>,<? extends T>, and<? super T>? - Explain the PECS principle with an example.
- What is the difference between a generic method and a generic class?
- Can you use primitives as type parameters? Why not?
- What are bounded type parameters?
- What happens if you try to add an element to
List<?>?