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.

Why Generics?

Before generics, collections stored Object references, requiring explicit casts and risking ClassCastException at runtime:

  List list = new ArrayList();
list.add("hello");
Integer num = (Integer) list.get(0); // ClassCastException at runtime
  

With generics, the compiler catches type errors early:

  List<String> list = new ArrayList<>();
list.add("hello");
// list.add(42);        // Compile error
String s = list.get(0); // No cast needed
  

Generic Classes and Interfaces

  public class Box<T> {
    private T value;

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

    public T getValue() {
        return value;
    }
}

Box<String> stringBox = new Box<>("Hello");
Box<Integer> intBox = new Box<>(42);
  

Common generic interfaces include List<T>, Map<K, V>, and Comparable<T>.

Generic Methods

A method can have its own type parameter, independent of the class:

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

The type parameter <T> appears before the return type.

Bounded Type Parameters

Restrict the types that can be used as type arguments:

  // Upper bound: T must be Number or a subclass
public static <T extends Number> double sum(T[] numbers) {
    double total = 0;
    for (T n : numbers) {
        total += n.doubleValue();
    }
    return total;
}
  

Multiple bounds are allowed: <T extends Number & Comparable<T>>.

Wildcards

Wildcards add flexibility when the exact type is unknown.

Wildcard Meaning
? Unknown type
? extends T Upper bounded — read-only producer
? super T Lower bounded — write-only consumer
  // PECS: Producer Extends, Consumer Super
public static double sumList(List<? extends Number> list) {
    double total = 0;
    for (Number n : list) {
        total += n.doubleValue();
    }
    return total;
}

public static void addIntegers(List<? super Integer> list) {
    list.add(42);
}
  

Type Erasure

Generics are a compile-time feature. At runtime, type parameters are erased to their bounds (or Object):

  List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
// Both have the same runtime type: List
System.out.println(strings.getClass() == integers.getClass()); // true
  

Because of erasure, you cannot:

  • Create generic arrays: new T[10] is illegal
  • Use instanceof with parameterized types: obj instanceof List<String> is illegal
  • Create instances of type parameters: new T() is illegal

Common Patterns

Generic Singleton Pattern

  public class GenericFactory {
    private static final Object LOCK = new Object();

    @SuppressWarnings("unchecked")
    public static <T> T[] emptyArray() {
        return (T[]) new Object[0];
    }
}
  

Type Token Workaround

When you need runtime type information, pass a Class<T> token:

  public static <T> T createInstance(Class<T> clazz) throws Exception {
    return clazz.getDeclaredConstructor().newInstance();
}

String s = createInstance(String.class);
  

Best Practices

  • Prefer generic types over raw types
  • Use bounded wildcards in public APIs (PECS principle)
  • Avoid mixing raw and parameterized types
  • Use @SuppressWarnings("unchecked") sparingly and only when necessary