Skip to main content
Java intermediate Lesson 23 of 58

Generics in Java

Deep dive into Java generics — generic methods, bounded type parameters, wildcards (? extends, ? super), type erasure, and the PECS principle.

Why Generics?

Without generics, collections stored Object and required explicit casts — with no compile-time type safety:

// Pre-generics (Java 1.4) — error-prone
List names = new ArrayList();
names.add("Alice");
names.add(42);           // compiles — bug hiding here

String name = (String) names.get(0); // OK
String bad  = (String) names.get(1); // ClassCastException at runtime!

// With generics — type checked at compile time
List<String> safeNames = new ArrayList<>();
safeNames.add("Alice");
// safeNames.add(42);   // compile error — caught immediately
String first = safeNames.get(0); // no cast needed

Generic Classes

// A generic pair that holds two values of potentially different types
public class Pair<A, B> {
    private final A first;
    private final B second;

    public Pair(A first, B second) {
        this.first  = first;
        this.second = second;
    }

    public A first()  { return first; }
    public B second() { return second; }

    public Pair<B, A> swap() { return new Pair<>(second, first); }

    @Override public String toString() {
        return "(" + first + ", " + second + ")";
    }
}

// Usage
Pair<String, Integer> nameAge = new Pair<>("Alice", 30);
System.out.println(nameAge.first());  // Alice
System.out.println(nameAge.second()); // 30
System.out.println(nameAge.swap());   // (30, Alice)

Generic Methods

Generic methods declare their type parameter before the return type:

public class GenericMethods {

    // Type parameter <T> is scoped to this method
    public static <T> T firstNonNull(T a, T b) {
        return a != null ? a : b;
    }

    // Multiple type parameters
    public static <K, V> Map<V, K> invertMap(Map<K, V> map) {
        Map<V, K> inverted = new HashMap<>();
        map.forEach((k, v) -> inverted.put(v, k));
        return inverted;
    }

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

    public static void main(String[] args) {
        System.out.println(firstNonNull(null, "default")); // default
        System.out.println(firstNonNull("value", "default")); // value

        Map<String, Integer> codes = Map.of("HTTP", 200, "NOT_FOUND", 404);
        Map<Integer, String> inverted = invertMap(codes);
        System.out.println(inverted.get(200)); // HTTP

        Integer[] arr = {1, 2, 3, 4, 5};
        swap(arr, 0, 4);
        System.out.println(Arrays.toString(arr)); // [5, 2, 3, 4, 1]
    }
}

Bounded Type Parameters

Upper Bound — <T extends SomeType>

// T must be a Number or subclass (Integer, Double, Long, etc.)
public static <T extends Number> double sum(List<T> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

System.out.println(sum(List.of(1, 2, 3)));         // 6.0
System.out.println(sum(List.of(1.5, 2.5, 3.0)));   // 7.0
// sum(List.of("a", "b")); // compile error — String is not a Number

// Multiple bounds — T must extend Comparable AND Serializable
public static <T extends Comparable<T> & java.io.Serializable> T clamp(T val, T min, T max) {
    if (val.compareTo(min) < 0) return min;
    if (val.compareTo(max) > 0) return max;
    return val;
}

System.out.println(clamp(15, 1, 10)); // 10
System.out.println(clamp(5,  1, 10)); // 5
System.out.println(clamp(-3, 1, 10)); // 1

Wildcards

Unbounded Wildcard — <?>

// Accept any kind of List — for code that only uses Object methods
public static void printAll(List<?> list) {
    for (Object item : list) {
        System.out.println(item);
    }
}

printAll(List.of("a", "b", "c"));   // works
printAll(List.of(1, 2, 3));         // works

Upper-Bounded Wildcard — <? extends T> (Producer)

Use when you read from the structure (it produces values of type T):

// Works with List<Integer>, List<Double>, List<Long>, etc.
public static double sumList(List<? extends Number> list) {
    return list.stream().mapToDouble(Number::doubleValue).sum();
}

List<Integer> ints    = List.of(1, 2, 3);
List<Double>  doubles = List.of(1.5, 2.5);
System.out.println(sumList(ints));    // 6.0
System.out.println(sumList(doubles)); // 4.0

// You CANNOT add to a ? extends list (type is unknown)
// list.add(1.0); // compile error — could be List<Integer>!

Lower-Bounded Wildcard — <? super T> (Consumer)

Use when you write to the structure (it consumes values of type T):

// Works with List<Integer>, List<Number>, List<Object>
public static void addNumbers(List<? super Integer> list, int count) {
    for (int i = 1; i <= count; i++) {
        list.add(i); // safe — Integer is a subtype of whatever super type it is
    }
}

List<Integer> intList    = new ArrayList<>();
List<Number>  numList    = new ArrayList<>();
List<Object>  objList    = new ArrayList<>();

addNumbers(intList, 3);
addNumbers(numList, 3);
addNumbers(objList, 3);
// addNumbers(List<Double>, 3); // compile error — Integer is not a subtype of Double

PECS in Action

// Collections.copy — the canonical PECS example
// src is a Producer (we read from it) — ? extends T
// dest is a Consumer (we write to it) — ? super T
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}

List<Integer> source = List.of(1, 2, 3);
List<Number>  target = new ArrayList<>();
copy(target, source); // Integer extends Number — works
System.out.println(target); // [1, 2, 3]

Type Erasure

The compiler replaces type parameters with their upper bound (or Object if unbounded) in bytecode:

// Source code
List<String> strings = new ArrayList<>();
strings.add("hello");
String s = strings.get(0);

// After erasure (what the JVM actually sees)
List strings2 = new ArrayList();
strings2.add("hello");
String s2 = (String) strings2.get(0); // cast inserted by compiler

// Type erasure limitations
<T> void example(T t) {
    // These do NOT compile due to erasure:
    // T obj = new T();            // cannot instantiate T
    // if (t instanceof T) {}      // cannot check T at runtime
    // T[] arr = new T[10];        // cannot create generic array
    // Class<T> cls = T.class;     // T.class does not exist
}

Working Around Type Erasure with Class

// Pass the Class object explicitly to work around erasure
public class TypeSafeContainer<T> {
    private final Class<T> type;
    private final List<Object> items = new ArrayList<>();

    public TypeSafeContainer(Class<T> type) { this.type = type; }

    public void add(T item) { items.add(item); }

    public T get(int index) {
        return type.cast(items.get(index)); // safe cast using Class
    }

    public boolean contains(Object obj) {
        return type.isInstance(obj); // runtime type check
    }
}

TypeSafeContainer<String> c = new TypeSafeContainer<>(String.class);
c.add("hello");
System.out.println(c.get(0));           // hello
System.out.println(c.contains("world")); // false
System.out.println(c.contains(42));      // false

Generic Interfaces

// Generic repository interface
public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void deleteById(ID id);
}

// Concrete implementation
public class InMemoryUserRepository implements Repository<User, Long> {
    private final Map<Long, User> store = new HashMap<>();
    private long nextId = 1;

    @Override
    public Optional<User> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public List<User> findAll() {
        return List.copyOf(store.values());
    }

    @Override
    public User save(User user) {
        var saved = new User(nextId++, user.name(), user.email());
        store.put(saved.id(), saved);
        return saved;
    }

    @Override
    public void deleteById(Long id) {
        store.remove(id);
    }
}

Comparable and Comparator with Generics

record Student(String name, double gpa) implements Comparable<Student> {
    @Override
    public int compareTo(Student other) {
        return Double.compare(other.gpa, this.gpa); // descending by GPA
    }
}

List<Student> students = new ArrayList<>(List.of(
    new Student("Alice", 3.8),
    new Student("Bob",   3.5),
    new Student("Carol", 3.9)
));

Collections.sort(students); // uses Comparable
students.forEach(s -> System.out.printf("%s: %.1f%n", s.name(), s.gpa()));
// Carol: 3.9
// Alice: 3.8
// Bob:   3.5

// Comparator with generics
Comparator<Student> byName = Comparator.comparing(Student::name);
Comparator<Student> byGpa  = Comparator.comparingDouble(Student::gpa).reversed();
Comparator<Student> byGpaThenName = byGpa.thenComparing(byName);

students.sort(byGpaThenName);

Frequently Asked Questions

What is type erasure and why does it matter?
At compile time the compiler checks generic types and then erases them — the bytecode contains no type parameters. List<String> and List<Integer> are both just List at runtime. This means you cannot do: new T(), T.class, instanceof List<String>, or create generic arrays. It's the cost of backward compatibility with pre-generics Java.
What is the PECS principle?
Producer Extends, Consumer Super. If a parameterized type is a producer (you only read from it), use ? extends T. If it is a consumer (you only write to it), use ? super T. If it does both, use the exact type T. Example: Collections.copy(List<? super T> dest, List<? extends T> src).
What is the difference between List<?> and List<Object>?
List<Object> accepts only a List<Object> — you cannot pass a List<String> to it because generics are invariant. List<?> (unbounded wildcard) accepts any List regardless of type parameter — but you can only read from it as Object and cannot add elements (except null).
Can I use generics with primitive types?
No. Type parameters must be reference types. Use the wrapper classes: Integer for int, Double for double, etc. The JVM auto-boxes and unboxes, but this adds overhead. For performance-critical numeric code use primitive streams (IntStream, LongStream, DoubleStream) or specialised libraries.