☕ Java

Generic Class

A generic class in Java is a class parameterized by one or more type parameters, written as class Box<T> or class Pair<K, V>. Type parameters allow a single class definition to work correctly with many different types while preserving full compile-time type safety — eliminating the need for casts, preventing ClassCastException at runtime, and making the intended type contract explicit in the API. The compiler erases type parameters at bytecode level (type erasure), replacing them with their bounds or Object, but all type checking occurs at compile time before erasure. This entry covers type parameter declaration and naming conventions, bounded type parameters (extends, super), multiple type parameters, raw types and why they are unsafe, type erasure and its consequences, wildcards (?, ? extends T, ? super T), the PECS principle, recursive type bounds, generic interfaces, inheritance with generic classes, and how to design generic classes that work correctly across all parameterizations.

Type Parameter Declaration, Naming, and Bounds

A generic class is declared by placing one or more type parameter names in angle brackets immediately after the class name: class Box<T>, class Pair<K, V>, class Triple<A, B, C>. By convention, single uppercase letters are used for type parameters: T for a general type, E for element (used by collection classes), K and V for key and value (used by Map), N for number, and R for return type. These conventions are not enforced by the compiler but are universally followed and aid readability. Type parameters are in scope throughout the class body: in field declarations, constructor parameters, method return types, method parameter types, and local variable types. They are not in scope in static contexts — static fields and static methods cannot use the class's type parameters, because static members belong to the class itself rather than to any particular parameterization of the class. A static member shared across Box<String>, Box<Integer>, and Box<Double> cannot meaningfully reference T. An unbounded type parameter T accepts any reference type. A bounded type parameter restricts the set of acceptable types. An upper bound T extends Comparable<T> means T must be a subtype of Comparable<T> — numeric types, String, and any class implementing Comparable qualify. An upper bound can extend one class and multiple interfaces: T extends Number & Comparable<T> & Serializable. The class bound, if any, must come first. Lower bounds (super) cannot be applied to type parameters in class declarations — they appear only in wildcard usage. When a type parameter has an upper bound, the type parameter's methods from that bound become available within the class. Inside class SortedBox<T extends Comparable<T>>, you can call t.compareTo(other) because the compiler knows T has that method. Without the bound, only Object's methods are available on a value of type T.
Java
// ── Basic generic class ───────────────────────────────────────────────
public class Box<T> {
    private T value;

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

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

Box<String>  strBox = new Box<>("hello");
Box<Integer> intBox = new Box<>(42);

String s = strBox.get();   // no cast needed — compile-time type safety
int    n = intBox.get();   // unboxed automatically

// ── Multiple type parameters ───────────────────────────────────────────
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; }

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

Pair<String, Integer> p = new Pair<>("age", 30);
System.out.println(p.first());   // age
System.out.println(p.second());  // 30

// ── Upper bounded type parameter ──────────────────────────────────────
public class SortedBox<T extends Comparable<T>> {
    private T value;

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

    public boolean isGreaterThan(T other) {
        return value.compareTo(other) > 0;  // compareTo available because T extends Comparable<T>
    }

    public T get() { return value; }
}

SortedBox<Integer> sb = new SortedBox<>(10);
System.out.println(sb.isGreaterThan(5));   // true
System.out.println(sb.isGreaterThan(20));  // false

// SortedBox<StringBuilder> won't compile — StringBuilder doesn't implement Comparable

// ── Multiple bounds ───────────────────────────────────────────────────
public class NumericBox<T extends Number & Comparable<T>> {
    private T value;

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

    public double doubleValue() {
        return value.doubleValue();   // from Number bound
    }

    public boolean isGreaterThan(T other) {
        return value.compareTo(other) > 0;  // from Comparable bound
    }
}

NumericBox<Double> db = new NumericBox<>(3.14);
System.out.println(db.doubleValue());        // 3.14
System.out.println(db.isGreaterThan(2.71));  // true

// ── Static context cannot reference T ─────────────────────────────────
public class Registry<T> {
    private T instance;

    // COMPILE ERROR — static field cannot use T:
    // private static T shared;  // error: non-static type variable T cannot be referenced from a static context

    // COMPILE ERROR — static method cannot use T:
    // public static T create() { ... }

    // OK — instance member uses T:
    public T getInstance() { return instance; }
}

Type Erasure, Raw Types, and Runtime Behavior

Java generics are implemented via type erasure. The compiler uses type parameters for all type checking at compile time, then erases them from the bytecode, replacing each type parameter with its leftmost bound (or Object if unbounded). Box<String> and Box<Integer> compile to identical bytecode; at runtime, both are simply Box. The compiler inserts casts at use sites where needed — when you call box.get() and assign the result to String s, the compiler emits a checkcast instruction in the bytecode at the assignment site. Type erasure has several runtime consequences. First, instanceof checks against parameterized types are not possible: box instanceof Box<String> is a compile error. You can only check box instanceof Box (raw type) or use an unbounded wildcard box instanceof Box<?>. Second, you cannot create instances of a type parameter: new T() and new T[10] both fail to compile, because T is erased and the JVM has no way to know what constructor to call. The standard workarounds are passing a Class<T> token (a type token) or passing a Supplier<T> factory. Third, you cannot create generic arrays directly: new T[] fails, and new ArrayList<String>[10] (generic array creation) also fails — though (ArrayList<String>[]) new ArrayList<?>[10] with an unchecked cast compiles with a warning. Raw types are parameterized classes used without type arguments: Box rawBox = new Box("hello"). Raw types exist solely for backward compatibility with pre-generics Java code and should never be used in new code. Assigning a parameterized type to a raw type, or calling methods through a raw type reference, causes the compiler to emit unchecked warnings and disables all generic type checking for that reference. A raw Box's get() returns Object; a raw List's add() accepts Object. This can introduce heap pollution — a generic collection that contains elements of the wrong type — which surfaces as a ClassCastException at a point in the code far from where the bad element was inserted, making bugs extremely hard to diagnose. The @SuppressWarnings("unchecked") annotation suppresses unchecked cast warnings when you have verified through other means that the cast is safe. It should be used at the narrowest scope possible (local variable rather than entire method) and always accompanied by a comment explaining why the cast is safe.
Java
// ── Type erasure — same bytecode for all parameterizations ──────────
Box<String>  s = new Box<>("hi");
Box<Integer> i = new Box<>(42);

// At runtime, both are just "Box" — type parameters erased:
System.out.println(s.getClass());                      // class Box
System.out.println(s.getClass() == i.getClass());      // true — same class

// instanceof with raw type is allowed:
System.out.println(s instanceof Box);    // true
// instanceof with parameterized type is a compile error:
// System.out.println(s instanceof Box<String>);  // ERROR

// ── Cannot create instances of type parameters ─────────────────────────
public class Container<T> {
    // COMPILE ERROR:
    // private T instance = new T();      // cannot instantiate type parameter

    // Workaround 1: Class token
    private final Class<T> type;
    private T instance;

    public Container(Class<T> type) throws Exception {
        this.type     = type;
        this.instance = type.getDeclaredConstructor().newInstance();
    }

    // Workaround 2: Supplier factory (preferred in modern Java)
    public static <T> Container2<T> of(Supplier<T> factory) {
        return new Container2<>(factory.get());
    }
}

Container<ArrayList> c = new Container<>(ArrayList.class);
// Using supplier:
Box<StringBuilder> sb = Box.of(StringBuilder::new);

// ── Raw types disable all type checking ───────────────────────────────
Box<String> typed = new Box<>("hello");
Box raw = typed;                    // raw type assignment — unchecked warning
raw.set(42);                        // no compile error — raw Box.set takes Object
String result = typed.get();        // ClassCastException at RUNTIME — not at raw.set()!
// The exception is misleading: it happens at .get() not at .set(42)

// ── Heap pollution example ────────────────────────────────────────────
List<String> strings = new ArrayList<>();
List raw2 = strings;                // heap pollution begins
raw2.add(42);                       // puts Integer into a List<String>

for (String str : strings) {
    System.out.println(str.length()); // ClassCastException here — confusing!
}

// ── Generic array workaround ──────────────────────────────────────────
public class GenericStack<T> {
    private Object[] elements;  // Object[] instead of T[]
    private int size = 0;

    @SuppressWarnings("unchecked")
    public GenericStack(int capacity) {
        elements = new Object[capacity];  // safe: only T values ever stored
    }

    public void push(T item) { elements[size++] = item; }

    @SuppressWarnings("unchecked")
    public T pop() {
        // Safe: elements[--size] is always a T because push() enforces it
        return (T) elements[--size];
    }
}

GenericStack<String> stack = new GenericStack<>(10);
stack.push("hello");
stack.push("world");
System.out.println(stack.pop());  // world — no cast warning at call site

Wildcards, PECS, and Generic Class Inheritance

Wildcards (?) express unknown type arguments in generic class usage. An unbounded wildcard Box<?> means "a Box of some unknown type." A Box<?> can be read from (get() returns Object), but nothing can be written to it (set() cannot be called because the type is unknown — the compiler cannot verify that what you are inserting is the correct type). Wildcards are used in method parameter types when the method only needs to read from the collection/container, or when the code genuinely works with any parameterization. An upper-bounded wildcard Box<? extends Number> means "a Box of some type that is Number or a subtype of Number." You can read from it (get() returns Number), but you still cannot write to it (because ? extends Number might be Box<Integer>, Box<Double>, or Box<Float> — the compiler cannot know which, so no specific type can be safely inserted). Upper-bounded wildcards make a parameter covariant: a Box<Integer> is acceptable where Box<? extends Number> is expected, even though Box<Integer> is not a subtype of Box<Number>. A lower-bounded wildcard Box<? super Integer> means "a Box of some type that is Integer or a supertype of Integer." You can write Integer values into it (because whatever the actual type is, it can hold an Integer), but you can only read Object from it (because the actual type could be Box<Integer>, Box<Number>, or Box<Object> — only Object is guaranteed). Lower-bounded wildcards make a parameter contravariant. The PECS principle (Producer Extends, Consumer Super) summarizes when to use each: if a parameterized type produces (provides) T values for your code to read, use ? extends T; if it consumes (accepts) T values that your code writes into it, use ? super T. A method that copies from a source list to a destination list: static <T> void copy(List<? super T> dest, List<? extends T> src) — dest consumes T (super), src produces T (extends). Generic class inheritance has an important rule: Box<Integer> is NOT a subtype of Box<Number>, even though Integer is a subtype of Number. Generic types are invariant. Only wildcards introduce covariance (? extends) or contravariance (? super). A class can extend a generic class with a fixed type argument (class IntBox extends Box<Integer>), with its own type parameter (class SpecialBox<T> extends Box<T>), or with a bounded type parameter (class NumberBox<T extends Number> extends Box<T>).
Java
// ── Unbounded wildcard — read only, type is unknown ──────────────────
public static void printBox(Box<?> box) {
    Object value = box.get();   // OK — get() returns Object for Box<?>
    System.out.println(value);
    // box.set(something);  // COMPILE ERROR — cannot write to Box<?>
}

printBox(new Box<>("hello"));   // OK
printBox(new Box<>(42));        // OK
printBox(new Box<>(3.14));      // OK — any Box<?> accepted

// ── Upper-bounded wildcard — covariant read ───────────────────────────
public static double sumBoxes(List<Box<? extends Number>> boxes) {
    double sum = 0;
    for (Box<? extends Number> box : boxes) {
        sum += box.get().doubleValue();  // get() returns Number — doubleValue() available
    }
    return sum;
}

List<Box<Integer>> intBoxes    = List.of(new Box<>(1), new Box<>(2), new Box<>(3));
List<Box<Double>>  doubleBoxes = List.of(new Box<>(1.1), new Box<>(2.2));

// Both work — Box<Integer> and Box<Double> are subtypes of Box<? extends Number>:
// (But List<Box<Integer>> is NOT a subtype of List<Box<? extends Number>> — need cast or wildcard at list level)
// Full covariance at list level:
public static double sumBoxList(List<? extends Box<? extends Number>> list) {
    return list.stream()
        .mapToDouble(b -> b.get().doubleValue())
        .sum();
}
System.out.println(sumBoxList(intBoxes));     // 6.0
System.out.println(sumBoxList(doubleBoxes));  // 3.3

// ── Lower-bounded wildcard — contravariant write ──────────────────────
public static void fillBox(Box<? super Integer> box, Integer value) {
    box.set(value);             // OK — Box<? super Integer> can accept Integer
    Object v = box.get();       // only Object comes back — type is unknown supertype
}

Box<Integer> intBox    = new Box<>(0);
Box<Number>  numBox    = new Box<>(0.0);
Box<Object>  objBox    = new Box<>(null);

fillBox(intBox, 42);   // OK
fillBox(numBox, 42);   // OK — Number is a supertype of Integer
fillBox(objBox, 42);   // OK — Object is a supertype of Integer

// ── PECS — copy from producer (extends) to consumer (super) ──────────
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {   // src produces T
        dest.add(item);    // dest consumes T
    }
}

List<Number> destination = new ArrayList<>();
List<Integer> source     = List.of(1, 2, 3, 4, 5);
copy(destination, source);
System.out.println(destination);  // [1, 2, 3, 4, 5]

// ── Generic class inheritance ─────────────────────────────────────────
// Invariance: Box<Integer> is NOT a subtype of Box<Number>
Box<Number> numBox2 = new Box<>(42);
// Box<Number> numBox3 = new Box<Integer>(42);  // COMPILE ERROR

// Fixed type argument inheritance:
class IntBox extends Box<Integer> {
    public IntBox(Integer value) { super(value); }
    public int doubled() { return get() * 2; }
}

// Propagated type parameter:
class TaggedBox<T> extends Box<T> {
    private final String tag;
    public TaggedBox(T value, String tag) { super(value); this.tag = tag; }
    public String getTag() { return tag; }
}

// Bounded type parameter:
class NumericBox2<T extends Number> extends Box<T> {
    public NumericBox2(T value) { super(value); }
    public double asDouble() { return get().doubleValue(); }
}

TaggedBox<String>  tBox = new TaggedBox<>("content", "important");
NumericBox2<Float> fBox = new NumericBox2<>(3.14f);
System.out.println(tBox.getTag());     // important
System.out.println(fBox.asDouble());   // 3.140000104904175

Related Topics in Generics

Generic Method
A generic method is a method that declares its own type parameters, independent of any type parameters on its enclosing class. The type parameters appear in angle brackets before the return type: public static <T> T identity(T value). Generic methods can appear in non-generic classes, in generic classes (adding their own type parameters beyond the class's), and as both static and instance methods. The compiler infers the type arguments for generic method calls from the argument types, eliminating the need for explicit type witness syntax in most cases. This entry covers type parameter declaration on methods, type inference rules and when inference fails, bounded type parameters on methods, returning computed type relationships, generic methods that operate on multiple type parameters, static generic utility methods (the standard library pattern), the interaction between method type parameters and class type parameters, explicit type witness syntax, and the difference between using a class type parameter vs a method type parameter.
Type Parameters
Type parameters are the placeholders — written as single uppercase letters like T, E, K, V — that make Java generics work. A type parameter declares a variable that stands in for a concrete type, to be supplied by the caller or inferred by the compiler. They appear on classes (class Box<T>), interfaces (interface Comparable<T>), and methods (public static <T> T identity(T value)), and they are the mechanism by which the Java type system achieves compile-time type safety without duplicating code for each concrete type. This entry covers where and how type parameters are declared, the naming conventions and why they exist, the scope rules for type parameters (where they are in scope and where they are not), how type parameters interact with inheritance, how the compiler resolves a type parameter to a concrete type at each call site, and the distinction between a type parameter (a declaration) and a type argument (a concrete substitution).
Bounded Types
Bounded type parameters constrain which concrete types may be substituted for a type parameter. An upper bound (T extends Number) restricts T to Number and its subtypes, making Number's methods available on values of type T within the generic code. A lower bound cannot be applied to type parameters (only to wildcards), but multiple upper bounds can be combined with & (T extends Number & Comparable<T> & Serializable). Bounds serve two purposes simultaneously: they restrict the valid type arguments that callers can supply, and they expand what the generic code can do with values of type T by exposing the bound's API. Without a bound, only Object's methods are available on T values. This entry covers single and multiple upper bounds, recursive bounds, bounds on class type parameters vs method type parameters, how bounds interact with type erasure, bridge methods generated by the compiler, and practical patterns where bounds enable algorithms impossible with unbounded type parameters.
Wildcards
Wildcards — written as ? — represent an unknown type in a generic type argument position. Where a type parameter T is a name given to an unknown type that can be referenced throughout a declaration, a wildcard ? is an anonymous unknown type used at the point of consumption. Wildcards appear in method parameter types, field types, return types, and variable declarations — anywhere a parameterized type appears as a type (not where a type is being declared). The three wildcard forms are: the unbounded wildcard (List<?>), the upper-bounded wildcard (List<? extends Number>), and the lower-bounded wildcard (List<? super Integer>). Wildcards are the mechanism that introduces covariance and contravariance into Java's otherwise invariant generic type system. This entry covers all three forms in depth, the producer-extends consumer-super (PECS) principle, wildcard capture and the capture helper pattern, nested wildcards, and when to use wildcards vs type parameters.