Generics in Java

Type-safe reusable code: "If it compiles, the types are correct"

The Problem Generics Solve

// Java before generics - everything was Object
List names = new ArrayList();
names.add("Alice");
names.add("Bob");
names.add(42);  // Compiles! But wrong.

// You had to cast on every retrieval
String first = (String) names.get(0);  // Cast required
String oops = (String) names.get(2);   // ClassCastException at RUNTIME!

// The compiler couldn't help you - errors discovered in production
// With generics - type-safe collections
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add(42);  // COMPILE ERROR: incompatible types

// No casts needed - compiler knows the type
String first = names.get(0);  // No cast!

// Error caught at compile time, not in production at 3am

The Core Value Proposition

Generics move type checking from runtime (crashes in production) to compile time (IDE shows error immediately). This is a fundamental safety improvement for large systems.

Generic Classes

from typing import Generic, TypeVar

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, value: T):
        self.value = value

    def get(self) -> T:
        return self.value

# Type hints are optional, not enforced at runtime
box: Box[str] = Box("hello")
box.get()  # "hello"

# This "works" at runtime even though types are wrong
box: Box[str] = Box(123)  # Type checker warns, but runs
/**
 * A generic container class.
 * T is a type parameter - a placeholder for a real type.
 */
public class Box<T> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T get() {
        return value;
    }

    public void set(T value) {
        this.value = value;
    }
}

// Usage - compiler enforces types
Box<String> stringBox = new Box<>("hello");
String s = stringBox.get();  // No cast needed

Box<Integer> intBox = new Box<>(42);
Integer i = intBox.get();

// Type mismatch caught at compile time
Box<String> box = new Box<>(123);  // COMPILE ERROR

Generic Methods

public class Utilities {

    // Generic method - T is declared before return type
    public static <T> T firstOrNull(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    // Multiple type parameters
    public static <K, V> Map<V, K> invert(Map<K, V> map) {
        Map<V, K> inverted = new HashMap<>();
        for (Map.Entry<K, V> entry : map.entrySet()) {
            inverted.put(entry.getValue(), entry.getKey());
        }
        return inverted;
    }

    // Generic method in non-generic class
    public static <T> List<T> repeat(T item, int times) {
        List<T> result = new ArrayList<>();
        for (int i = 0; i < times; i++) {
            result.add(item);
        }
        return result;
    }
}

// Type inference - compiler figures out T
String first = Utilities.firstOrNull(List.of("a", "b"));  // T = String
Integer num = Utilities.firstOrNull(List.of(1, 2, 3));     // T = Integer

// Explicit type (rarely needed)
String s = Utilities.<String>firstOrNull(myList);

Bounded Type Parameters

Constrain what types can be used with your generic:

// T must be a Number or subclass (Integer, Double, etc.)
public class Statistics<T extends Number> {
    private final List<T> values;

    public Statistics(List<T> values) {
        this.values = values;
    }

    public double average() {
        // We can call doubleValue() because T extends Number
        return values.stream()
            .mapToDouble(Number::doubleValue)  // This works!
            .average()
            .orElse(0);
    }
}

// Usage
new Statistics<>(List.of(1, 2, 3));        // OK: Integer extends Number
new Statistics<>(List.of(1.5, 2.5));       // OK: Double extends Number
new Statistics<>(List.of("a", "b"));       // COMPILE ERROR: String not Number

// Multiple bounds
public <T extends Comparable<T> & Serializable> T max(T a, T b) {
    return a.compareTo(b) > 0 ? a : b;
}
// T must implement BOTH Comparable AND Serializable

Wildcards: The Power of "Unknown Type"

This is where generics get tricky. There are three types of wildcards:

// ? means "any type" - use when you don't care about the specific type

public void printAll(List<?> items) {
    for (Object item : items) {
        System.out.println(item);
    }
}

// Works with any List
printAll(List.of("a", "b"));
printAll(List.of(1, 2, 3));
printAll(List.of(new User(), new User()));

// But you can't add to a List (except null)
public void addItem(List<?> items) {
    items.add("hello");  // COMPILE ERROR
    // Why? The list might be List<Integer>!
}
//  = "T or any subtype of T"
// Use when you READ from a generic structure (producer)

public double sumAll(List<? extends Number> numbers) {
    double sum = 0;
    for (Number n : numbers) {  // Safe: we know it's at least Number
        sum += n.doubleValue();
    }
    return sum;
}

// Works with List<Integer>, List<Double>, List<Number>
sumAll(List.of(1, 2, 3));          // Integer extends Number
sumAll(List.of(1.5, 2.5));          // Double extends Number

// Still can't add (might be List<Integer>, can't add Double)
public void addNumber(List<? extends Number> numbers) {
    numbers.add(1.0);  // COMPILE ERROR
}
//  = "T or any supertype of T"
// Use when you WRITE to a generic structure (consumer)

public void addIntegers(List<? super Integer> list) {
    list.add(1);   // Safe: can always add Integer
    list.add(2);
    list.add(3);
}

// Works with List<Integer>, List<Number>, List<Object>
List<Number> numbers = new ArrayList<>();
addIntegers(numbers);  // OK: Number is supertype of Integer

List<Object> objects = new ArrayList<>();
addIntegers(objects);  // OK: Object is supertype of Integer

// But reading is limited - you only know it's Object
public void process(List<? super Integer> list) {
    Object item = list.get(0);  // Only know it's Object
    Integer i = list.get(0);    // COMPILE ERROR
}

PECS: Producer Extends, Consumer Super

This is one of the most commonly asked generics interview questions.

Real-World Example: PECS in Action

// From the actual Java Collections class
public static <T> void copy(
    List<? super T> dest,     // Consumer: we WRITE to dest
    List<? extends T> src    // Producer: we READ from src
) {
    for (T item : src) {
        dest.add(item);
    }
}

// This allows maximum flexibility:
List<Number> numbers = new ArrayList<>();
List<Integer> integers = List.of(1, 2, 3);

Collections.copy(numbers, integers);  // OK!
// - dest is List<Number>, Number is super of Integer ✓
// - src is List<Integer>, Integer extends Integer ✓

// Without wildcards, this wouldn't compile:
public static <T> void badCopy(List<T> dest, List<T> src) { ... }
badCopy(numbers, integers);  // COMPILE ERROR: List<Number> != List<Integer>

Type Erasure: The Java Compromise

Java generics are implemented via "type erasure" - generic type info is removed at runtime. This was for backwards compatibility with pre-generics code.

// What you write:
List<String> strings = new ArrayList<String>();
strings.add("hello");
String s = strings.get(0);

// What the compiler sees after erasure:
List strings = new ArrayList();
strings.add("hello");
String s = (String) strings.get(0);  // Cast inserted by compiler

// Consequences:

// 1. Can't create generic arrays
T[] array = new T[10];  // COMPILE ERROR

// 2. Can't use instanceof with generics
if (obj instanceof List<String>) { }  // COMPILE ERROR
if (obj instanceof List<?>) { }      // OK (wildcard)

// 3. Can't distinguish generic types at runtime
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
strings.getClass() == integers.getClass();  // TRUE! Both are just ArrayList

// 4. Static fields can't use type parameters
public class Box<T> {
    private static T instance;  // COMPILE ERROR
}

Interview Topic: Why Type Erasure?

When generics were added in Java 5, there was a decade of existing non-generic code. Type erasure allowed generic and non-generic code to interoperate. The tradeoff was losing type information at runtime.

Contrast with C#/.NET reified generics which preserve type info at runtime (but broke compatibility).

Generic Patterns for API Design

Pattern 1: The Builder with Generics

// Self-referential generics for fluent builders
public abstract class Builder<T, B extends Builder<T, B>> {
    protected String name;

    @SuppressWarnings("unchecked")
    protected B self() {
        return (B) this;
    }

    public B name(String name) {
        this.name = name;
        return self();
    }

    public abstract T build();
}

// Subclass builder returns correct type
public class UserBuilder extends Builder<User, UserBuilder> {
    private String email;

    public UserBuilder email(String email) {
        this.email = email;
        return this;
    }

    @Override
    public User build() {
        return new User(name, email);
    }
}

// Fluent API works correctly
User user = new UserBuilder()
    .name("Alice")      // Returns UserBuilder, not Builder
    .email("a@b.com")  // So this compiles!
    .build();

Pattern 2: Type-Safe Heterogeneous Container

// Store different types with type safety (no casts needed)
public class TypeSafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();

    public <T> void put(Class<T> type, T instance) {
        map.put(type, instance);
    }

    public <T> T get(Class<T> type) {
        return type.cast(map.get(type));  // Safe cast via Class.cast()
    }
}

// Usage - type-safe, no casts needed
TypeSafeMap context = new TypeSafeMap();
context.put(String.class, "hello");
context.put(Integer.class, 42);
context.put(User.class, new User("Alice"));

String s = context.get(String.class);   // "hello" - no cast!
Integer i = context.get(Integer.class); // 42 - no cast!
User u = context.get(User.class);       // User - no cast!

// Used in many frameworks (Spring, Guice, etc.)

Common Interview Questions

QuestionAnswer
What is type erasure? Generic type info is removed at compile time. List<String> becomes List at runtime.
Why can't you create new T()? Type erasure - T is unknown at runtime. Use factory pattern or Class<T> parameter.
Difference between <T> and <?>? T is a type parameter you can reference. ? is an unknown type you can't reference.
What is PECS? Producer Extends, Consumer Super. Use extends when reading, super when writing.
Can you have generic exceptions? No - exception types can't be generic due to erasure and catch semantics.