Java Generics

Java generics let you write classes and methods that work with any type while keeping full type safety. They are why List<String> only accepts strings and why you never need a cast when you read from it.

Why Generics?

Without generics, a list holds plain Object values and you must cast every item, risking a ClassCastException at runtime. With generics, the compiler catches type mistakes before the program runs.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Zara");
        // names.add(42); // compile error: int cannot be converted to String
        String first = names.get(0); // no cast needed
        System.out.println(first.toUpperCase());
    }
}
ZARA

Writing a Generic Class

Declare a type parameter in angle brackets after the class name. T is a placeholder that the user fills in.

class Box<T> {
    private final T value;

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

    T get() { return value; }

    boolean isEmpty() { return value == null; }
}

class Pair<K, V> {
    final K key; final V value;
    Pair(K key, V value) { this.key = key; this.value = value; }
    @Override public String toString() { return key + "=" + value; }
}

public class Main {
    public static void main(String[] args) {
        Box<Integer> numberBox = new Box<>(7);
        Box<String> textBox = new Box<>("hi");
        System.out.println(numberBox.get() * 6);
        System.out.println(textBox.get().length());
        System.out.println(new Pair<>("age", 30));
    }
}
42
2
age=30

Generic Methods and Bounded Types

A method can declare its own type parameter before the return type. A bound like <T extends Comparable<T>> restricts T to types that can be compared, so you can call compareTo on them.

import java.util.List;

public class Main {
    static <T> T firstOrDefault(List<T> list, T fallback) {
        return list.isEmpty() ? fallback : list.get(0);
    }

    static <T extends Comparable<T>> T largest(List<T> items) {
        T best = items.get(0);
        for (T item : items) {
            if (item.compareTo(best) > 0) best = item;
        }
        return best;
    }

    public static void main(String[] args) {
        System.out.println(firstOrDefault(List.of(), "none"));
        System.out.println(largest(List.of(4, 11, 7)));
        System.out.println(largest(List.of("pear", "apple", "plum")));
    }
}
none
11
plum

Wildcards

A wildcard ? means "some unknown type". List<? extends Number> accepts a list of Integer, Double or any Number subtype, which is ideal when you only read from it.

import java.util.List;

public class Main {
    static double sum(List<? extends Number> numbers) {
        double total = 0;
        for (Number n : numbers) total += n.doubleValue();
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(List.of(1, 2, 3)));
        System.out.println(sum(List.of(1.5, 2.5)));
    }
}
6.0
4.0

Naming Conventions

  • T: type, E: element (collections), K and V: key and value, R: return type.
  • Remember "PECS": Producer Extends, Consumer Super. Use ? extends T to read, ? super T to write.
  • Generic type information is erased at runtime, so you cannot write new T() or instanceof List<String>.

Frequently asked questions

What does <T> mean in Java?

It declares a type parameter named T, a placeholder for a real type supplied when the class or method is used, such as Box<String>.

What is the difference between <? extends T> and <? super T>?

? extends T accepts T or its subtypes and is safe for reading. ? super T accepts T or its supertypes and is safe for adding T values.

What is type erasure in Java?

The compiler checks generic types and then removes them, so at runtime a List<String> is just a List. This keeps compatibility with older Java code.