Deep Dive into Java Generics Mechanics and Constraints

Generic Class Declaration

A generic class introduces one or more type parameters within angle brackets. For example:

public class Range<T> {
    private T lower;
    private T upper;

    public Range(T low, T high) {
        this.lower = low;
        this.upper = high;
    }
}

Instantiation binds T to a concrete type at compile time:

Range<LocalDateTime> duration = new Range<>(start, end);

Inheritance with Generics

A subclass may retain genericity or fix the type parameter:

  • Generic subclass:

    public class TimeInterval<T> extends Range<T> { /* ... */ }
    
  • Concrete subclass:

    public class DateSpan extends Range<LocalDate> { /* ... */ }
    

The following is invalid — <LocalDate> after the class name has no meaning in Java:

// ❌ Compilation error
public class DateSpan<LocalDate> extends Range<LocalDate> {}

Generic Methods

Type parameters for methods appear before the return type:

public static <U> U identity(U value) {
    return value;
}

Due to type erasure, the bytecode contains no generic metadata. Instead, a Signature attribute preserves the generic signature for tools like decompilers.

Constructor-Level Type Parameters

Constructors can declare independent type variables, distinct from the enclosing class’s parameters:

class ConfigurableContainer<T> {
    public <K> ConfigurableContainer(K config) {
        // K is independent of T
    }
}

Usage:

ConfigurableContainer<String> container = new <Integer>ConfigurableContainer<>(42);
// K → Integer, T → String

Bounded Type Variables

Constraints restrict allowable types:

  • Single upper bound (class or interface):

    public class PositiveValue<T extends Number> {
        private final T value;
        public PositiveValue(T v) { this.value = v; }
        public boolean isPositive() { return value.doubleValue() > 0; }
    }
    

    Only Number and its subtypes (e.g., BigDecimal, Long) are permitted.

  • Multiple bounds, where at most one is a class (and must come first):

    interface Serializable {}
    class Base {}
    class Derived extends Base implements Serializable {}
    
    public static <X extends Base & Serializable> void process(X item) { /* ... */ }
    

    process(new Base()) fails; process(new Derived()) succeeds.

Wildcards

Wildcards (?) represent unknown types and support flexible subtyping.

Upper-Bounded Wildcard (? extends T)

Enables safe reading from collections:

public static double sumNumbers(List<? extends Number> nums) {
    return nums.stream().mapToDouble(Number::doubleValue).sum();
}

Valid calls:

sumNumbers(new ArrayList<Integer>());
sumNumbers(new ArrayList<Double>());
sumNumbers(new ArrayList<Number>());

But insertion is prohibited — the compiler cannot guarantee type safety:

// ❌ Compile-time error
nums.add(new Integer(1));

Unbounded Wildcard (?)

Indicates total type ignorance:

public static int sizeOfAnyList(List<?> list) {
    return list.size(); // only non-generic operations allowed
}

Elements retrieved via get() have type Object. Insertion is only valid for null.

Lower-Bounded Wildcard (? super T)

Enables safe writing:

public static void fillWithZeros(List<? super Integer> target, int count) {
    for (int i = 0; i < count; i++) {
        target.add(0); // OK: Integer is assignable to any supertype of Integer
    }
}

Accepts List<Integer>, List<Number>, or List<Object>.

Wildcard Capture

Direct mutation using ? fails due to type uncertainty:

// ❌ Compile error: incompatible types
public static void swapFirst(List<?> list) {
    if (list.size() >= 2) {
        list.set(0, list.get(1)); // CAP#1 ≠ CAP#2
    }
}

Solution: delegate to a generic helper method that captures the unknown type:

public static void swapFirst(List<?> list) {
    swapHelper(list);
}

private static <E> void swapHelper(List<E> list) {
    if (list.size() >= 2) {
        E temp = list.get(0);
        list.set(0, list.get(1));
        list.set(1, temp);
    }
}

Bridge Methods

After erasure, raw signatures may conflict with intended polymorphism. The compiler synthesizes bridge methods to preserve inheritance contracts.

Given:

class Node<T> {
    T data;
    void setData(T d) { this.data = d; }
}

class IntNode extends Node<Integer> {
    @Override
    void setData(Integer d) { /* ... */ }
}

Erasure produces:

class Node {
    Object data;
    void setData(Object d) { /* ... */ }
}

class IntNode extends Node {
    void setData(Integer d) { /* ... */ }
    // Compiler injects:
    void setData(Object d) { setData((Integer) d); }
}

Without the bridge, IntNode would not override Node.setData(Object).

Key Limitations

  • No primitive type arguments: List<int> is illegal; use boxed types (List<Integer>).

  • No generic array creation: new List<String>[10] fails — erased to new List[10], breaking runtime type checks.

  • No instanceof with parameterized types: list instanceof List<String> is disallowed; only raw types (List.class) exist at runtime.

  • No static fields with type parameters: static T cache violates type erasure — static members belong to the class, not parameterized instances.

  • No exception classes with type parameters: class ValidationException<T> extends Exception is forbidden — exception handling relies on exact class identity at runtime.

  • No overloading by generic parameter alone: void handle(List<String>) and void handle(List<Integer>) erase to identical signatures and cause compilation failure.

Heap Pollution

Occurs when unchecked conversions allow incorrect object assignments into generic structures. A common source is varargs with generic types:

public static <T> void addAll(List<T> dest, T... items) {
    Collections.addAll(dest, items);
}

public static void unsafeCall() {
    List<String> strings = new ArrayList<>();
    // ⚠️ Warning: heap pollution risk
    addAll(strings, "a", "b", 42); // 42 is not a String
}

Suppress warnings selectively:

  • @SafeVarargs: guarantees safety (applicable only to final, static, or private methods).
  • @SuppressWarnings({"unchecked", "varargs"}): suppresses both unchecked casts and varargs warnings.

Compiler Diagnositcs

Enable enhanced feedback during compilation:

  • -Xlint:unchecked: reports unchecked operations (e.g., raw type usage).
  • -Xdiags:verbose: increases diagnostic detail level.
  • -Duser.language=en: forces English locale for messages.

Tags: java generics type-erasure wildcards bridge-methods

Posted on Thu, 05 Nov 2026 16:31:45 +0000 by fatywombat