Generics let you write a class or method once and use it safely with many types: List<String> instead of a raw list that holds anything. They catch type errors at compile time and remove casts. This guide covers generic classes and methods, bounds and wildcards, and what type erasure means at run time.

In Simple Terms

Generics = Reusable containers:

A plastic bag can hold anything — apples, toys, clothes. But what if you labeled the bag: 'ONLY APPLES'. That's generics! You tell Java exactly what type to store, and it checks at compile time that you're not accidentally putting a banana in your apple bag.

Advertisement

Generic Classes & Methods

// Generic class: <T> is type parameter
public class Box<T> {
    private T content;

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

    @Override
    public String toString() { return "Box[" + content + "]"; }
}

Box<String> stringBox = new Box<>();
stringBox.set("Hello Java");
String s = stringBox.get();  // No cast needed!

Box<Integer> intBox = new Box<>();
intBox.set(42);
// intBox.set("text");  // COMPILE ERROR! Wrong type

// Generic Method
public static <T extends Comparable<T>> T findMax(T[] arr) {
    T max = arr[0];
    for (T element : arr) {
        if (element.compareTo(max) > 0) {
            max = element;
        }
    }
    return max;
}

Integer max = findMax(new Integer[]{3, 1, 4, 1, 5, 9});  // 9
String maxStr = findMax(new String[]{"banana", "apple", "cherry"});  // cherry

// Multiple type parameters
public class Pair<A, B> {
    A first; B second;
    public Pair(A first, B second) {
        this.first = first; this.second = second;
    }
}
Pair<String, Integer> p = new Pair<>("Age", 25);

Wildcards & Bounds

// ── UPPER BOUNDED: <? extends T> ──
// Accept any type that IS a T or EXTENDS T
// Can READ (return T), cannot WRITE (except null)
public double sumList(List<? extends Number> list) {
    double sum = 0;
    for (Number n : list) { sum += n.doubleValue(); }
    return sum;
}
sumList(new ArrayList<Integer>());  // OK
sumList(new ArrayList<Double>());   // OK
sumList(new ArrayList<String>());   // COMPILE ERROR

// ── LOWER BOUNDED: <? super T> ──
// Accept any type that IS a T or is PARENT of T
// Can WRITE T objects, reading gives Object
public void addNumbers(List<? super Integer> list) {
    list.add(1); list.add(2); list.add(3);  // OK to add Integer
}
addNumbers(new ArrayList<Integer>());  // OK
addNumbers(new ArrayList<Number>());   // OK
addNumbers(new ArrayList<Object>());   // OK

// ── UNBOUNDED: <?> ──
// Any type; can only read as Object
public void printList(List<?> list) {
    for (Object o : list) System.out.println(o);
}

// PECS Rule: Producer Extends, Consumer Super
// If list PRODUCES values (you read): use extends
// If list CONSUMES values (you write): use super

Type Erasure

// At RUNTIME, all generic type information is ERASED
// List<String> and List<Integer> are SAME type at runtime: List

List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true!

// You CANNOT do:
// new T()             — don't know T at runtime
// new T[]             — can't create generic array
// if (obj instanceof T)  — type erased!
// List<String>.class     — doesn't exist

// What compiler does:
// Box<String> → Box   (raw type)
// T → Object (or bound type)
// Adds casts where needed
// Generates bridge methods for overriding

Interview Questions

What is type erasure in Java?

Generics are compile-time only. At runtime, all type parameters are erased and replaced with Object (or bound type). This maintains backward compatibility with pre-generics code. Consequence: cannot create generic arrays, use instanceof with generic types, or access T.class.

What is the PECS principle?

Producer Extends, Consumer Super. If a generic collection produces data you'll read — use upper bound (extends). If it consumes data you'll write — use lower bound (super). Example: Collections.copy(List<? super T> dest, List<? extends T> src).

Generics in Test Code

// A reusable API response wrapper used by REST Assured tests
public class ApiResponse<T> {
    private int status;
    private T data;
    public T getData() { return data; }
}

// Deserialize with a type reference so the generic type survives erasure
ApiResponse<User> res = given().get("/users/7")
        .as(new TypeRef<ApiResponse<User>>() { });
User user = res.getData();                       // no cast needed

// A generic helper that works for any page object
public static <T extends BasePage> T open(Class<T> page, WebDriver driver) {
    try {
        return page.getConstructor(WebDriver.class).newInstance(driver);
    } catch (ReflectiveOperationException e) {
        throw new IllegalStateException("Cannot create " + page.getSimpleName(), e);
    }
}

Because of type erasure, ApiResponse<User>.class doesn't exist at run time. Libraries such as REST Assured (TypeRef) and Jackson (TypeReference) use an anonymous subclass to capture the full generic type.