Skip to content

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:

List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);

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 arrays
  • List<int> — cannot use primitives (use List<Integer>)
  • T.class — cannot get type at runtime
  • instanceof 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<?>?