Skip to content

Java Generics

What are Generics?

Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. They provide compile-time type safety and eliminate the need for casting.

// Without generics (pre-Java 5)
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);  // Manual cast, runtime error if wrong type

// With generics
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0);  // No cast needed, compile-time type checking
// list.add(123);  // Compile error!

Generic Classes

Basic Generic Class

public class Box<T> {
    private T content;

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

    public T get() {
        return content;
    }
}

// Usage
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get();

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

Multiple Type Parameters

public class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }
}

// Usage
Pair<String, Integer> pair = new Pair<>("age", 25);
String key = pair.getKey();      // "age"
Integer value = pair.getValue();  // 25

Generic Class with Bounds

// T must extend Number (or be Number)
public class NumericBox<T extends Number> {
    private T value;

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

    public double doubleValue() {
        return value.doubleValue();  // Safe because T extends Number
    }
}

// Usage
NumericBox<Integer> intBox = new NumericBox<>(42);
NumericBox<Double> doubleBox = new NumericBox<>(3.14);
// NumericBox<String> stringBox = ...;  // Compile error!

Generic Methods

Basic Generic Method

public class Util {
    // Generic method (note <T> before return type)
    public static <T> T getFirst(List<T> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }

    // Generic method with multiple type parameters
    public static <K, V> V getValue(Map<K, V> map, K key) {
        return map.get(key);
    }
}

// Usage - type inference
String first = Util.getFirst(List.of("a", "b", "c"));
Integer num = Util.getFirst(List.of(1, 2, 3));

// Explicit type (rarely needed)
String first2 = Util.<String>getFirst(List.of("a", "b"));

Generic Method with Bounds

public class MathUtil {
    // T must extend Comparable<T>
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }

    // Multiple bounds (class first, then interfaces)
    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;
    }
}

// Usage
Integer maxInt = MathUtil.max(10, 20);  // 20
String maxStr = MathUtil.max("apple", "banana");  // "banana"

Generic Interfaces

public interface Repository<T, ID> {
    T findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(T entity);
}

// Implementation
public class UserRepository implements Repository<User, Long> {
    @Override
    public User findById(Long id) { ... }

    @Override
    public List<User> findAll() { ... }

    @Override
    public User save(User entity) { ... }

    @Override
    public void delete(User entity) { ... }
}

Wildcards

Unbounded Wildcard (?)

// Accepts any type
public void printList(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

// Usage
printList(List.of("a", "b", "c"));
printList(List.of(1, 2, 3));
printList(List.of(new Object()));

// Limitation: Can only read as Object, cannot write (except null)
public void addToList(List<?> list) {
    // list.add("hello");  // Compile error!
    // list.add(123);      // Compile error!
    list.add(null);        // OK (but rarely useful)
}

Upper Bounded Wildcard (? extends)

// Accepts Number or any subclass (Integer, Double, etc.)
public double sumOfList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) {
        sum += n.doubleValue();
    }
    return sum;
}

// Usage
sumOfList(List.of(1, 2, 3));           // List<Integer>
sumOfList(List.of(1.5, 2.5, 3.5));     // List<Double>
sumOfList(List.of(1L, 2L, 3L));        // List<Long>

// PRODUCER - can read, cannot write
public void cannotAdd(List<? extends Number> list) {
    Number n = list.get(0);            // OK - read as Number
    // list.add(1);                    // Compile error!
    // list.add(1.0);                  // Compile error!
}

Lower Bounded Wildcard (? super)

// Accepts Integer or any superclass (Number, Object)
public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
    list.add(3);
}

// Usage
List<Integer> integers = new ArrayList<>();
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();

addIntegers(integers);  // OK
addIntegers(numbers);   // OK
addIntegers(objects);   // OK

// CONSUMER - can write, limited read
public void cannotRead(List<? super Integer> list) {
    list.add(10);                      // OK - write Integer
    Object obj = list.get(0);          // Only read as Object
    // Integer i = list.get(0);        // Compile error!
}

PECS Principle

Producer Extends, Consumer Super

// If you need to READ from a collection → use extends (producer)
// If you need to WRITE to a collection → use super (consumer)
// If you need BOTH → use exact type (no wildcard)

public class Collections {
    // copy() reads from src (producer) and writes to dest (consumer)
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i = 0; i < src.size(); i++) {
            dest.set(i, src.get(i));
        }
    }
}

// Example
List<Number> numbers = new ArrayList<>(List.of(1.0, 2.0, 3.0));
List<Integer> integers = List.of(4, 5, 6);
Collections.copy(numbers, integers);  // Copies integers into numbers

Type Erasure

Generics are implemented through type erasure - generic type information is removed at compile time.

// What you write
public class Box<T> {
    private T content;
    public T get() { return content; }
}

// What compiler generates (after erasure)
public class Box {
    private Object content;
    public Object get() { return content; }
}

// With bounds
public class NumericBox<T extends Number> {
    private T value;
}

// After erasure
public class NumericBox {
    private Number value;  // Erased to bound
}

Consequences of Type Erasure

// 1. Cannot use instanceof with generic types
// if (list instanceof List<String>) { }  // Compile error!
if (list instanceof List<?>) { }          // OK

// 2. Cannot create generic arrays
// T[] array = new T[10];                  // Compile error!
T[] array = (T[]) new Object[10];         // Workaround (unchecked warning)

// 3. Cannot use primitive types
// List<int> list = ...;                   // Compile error!
List<Integer> list = new ArrayList<>();   // Use wrapper

// 4. Runtime type is same for all generic instantiations
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass());  // true!

Generic Type Restrictions

Cannot Do

// 1. Cannot instantiate generic type
public class Container<T> {
    // T item = new T();  // Compile error!

    // Workaround: Pass Class<T>
    public T createInstance(Class<T> clazz) throws Exception {
        return clazz.getDeclaredConstructor().newInstance();
    }
}

// 2. Cannot create array of generic type
// T[] items = new T[10];  // Compile error!

// 3. Cannot use primitives
// List<int> list = ...;  // Compile error!

// 4. Cannot use static fields with type parameter
public class Box<T> {
    // static T value;  // Compile error!
    // Because static is shared across all instances
}

// 5. Cannot catch or throw generic type
// catch (T exception) { }  // Compile error!

Practical Examples

Generic Stack

public class Stack<E> {
    private List<E> elements = new ArrayList<>();

    public void push(E item) {
        elements.add(item);
    }

    public E pop() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.remove(elements.size() - 1);
    }

    public E peek() {
        if (isEmpty()) {
            throw new EmptyStackException();
        }
        return elements.get(elements.size() - 1);
    }

    public boolean isEmpty() {
        return elements.isEmpty();
    }

    public int size() {
        return elements.size();
    }
}

// Usage
Stack<String> stringStack = new Stack<>();
stringStack.push("hello");
stringStack.push("world");
String top = stringStack.pop();  // "world"

Generic DAO Pattern

public interface GenericDao<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void delete(T entity);
    boolean existsById(ID id);
}

public abstract class AbstractDao<T, ID> implements GenericDao<T, ID> {
    protected Class<T> entityClass;

    @SuppressWarnings("unchecked")
    public AbstractDao() {
        // Get actual type argument at runtime (from subclass)
        ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass();
        this.entityClass = (Class<T>) type.getActualTypeArguments()[0];
    }

    @Override
    public List<T> findAll() {
        String query = "SELECT e FROM " + entityClass.getSimpleName() + " e";
        // Execute query...
        return null;
    }
}

public class UserDao extends AbstractDao<User, Long> {
    // Automatically knows entityClass is User
}

Generic Builder Pattern

public class Builder<T extends Builder<T>> {
    protected String name;
    protected int value;

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

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

    public T value(int value) {
        this.value = value;
        return self();
    }
}

public class AdvancedBuilder extends Builder<AdvancedBuilder> {
    private String extra;

    public AdvancedBuilder extra(String extra) {
        this.extra = extra;
        return self();
    }

    @Override
    protected AdvancedBuilder self() {
        return this;
    }
}

// Fluent chaining works!
AdvancedBuilder builder = new AdvancedBuilder()
    .name("test")
    .value(42)
    .extra("advanced");

Generic Utility Methods

public class GenericUtils {

    // Swap elements in array
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // Find max in collection
    public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
        Iterator<T> iterator = coll.iterator();
        T max = iterator.next();
        while (iterator.hasNext()) {
            T next = iterator.next();
            if (next.compareTo(max) > 0) {
                max = next;
            }
        }
        return max;
    }

    // Filter collection by predicate
    public static <T> List<T> filter(Collection<T> collection, Predicate<T> predicate) {
        List<T> result = new ArrayList<>();
        for (T item : collection) {
            if (predicate.test(item)) {
                result.add(item);
            }
        }
        return result;
    }

    // Convert list type
    public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
        List<R> result = new ArrayList<>();
        for (T item : list) {
            result.add(mapper.apply(item));
        }
        return result;
    }
}

Common Interview Questions

  1. What is type erasure?
  2. Compiler removes generic type info at compile time, replaces with Object (or bound)

  3. Why can't you create new T()?

  4. Type erasure - T becomes Object at runtime, compiler doesn't know actual type

  5. What's the difference between <?> and <Object>?

  6. <?> accepts any generic type, <Object> only accepts List<Object> exactly

  7. Explain PECS?

  8. Producer Extends, Consumer Super - for read use extends, for write use super

  9. Can generics be used with primitives?

  10. No, must use wrapper classes (Integer, Double, etc.)

  11. What's List<?> vs List<Object> vs List?

  12. List<?>: Any type, can only read as Object
  13. List<Object>: Specifically Object, can read/write Object
  14. List (raw): No type checking, legacy, avoid