📋 Section 20 · Question #15

What are bounded type parameters

By default, an unbounded type parameter allows any Java reference type (Object, String, Thread, etc.). When a bound is specified, only that type or its subtypes are accepted:

Bounded Type Parameters allow developers to restrict the permissible types that can be passed as generic type arguments using the extends keyword. In addition to acting as compile-time type filters, bounds grant generic methods and classes direct access to the public methods declared by the bounding classes or interfaces without manual downcasting.


🟢 Junior Level

Syntax and Basic Usage

By default, an unbounded type parameter <T> allows any Java reference type (Object, String, Thread, etc.). When a bound is specified, only that type or its subtypes are accepted:

// T must be java.lang.Number or any of its subclasses (Integer, Double, BigDecimal, etc.)
public class NumberBox<T extends Number> {
    private T value;

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

    public double getAsDouble() {
        // Safe: Methods of java.lang.Number are directly accessible on T!
        return value.doubleValue();
    }
}

// Valid invocations:
NumberBox<Integer> intBox = new NumberBox<>();
NumberBox<Double> dblBox = new NumberBox<>();

// Compilation Error: Type parameter 'String' is not within its bound
// NumberBox<String> strBox = new NumberBox<>();

The Universal extends Keyword

In Java Generics, the keyword extends is used universally for both classes and interfaces:

  • Class bound: <T extends Number>
  • Interface bound: <T extends AutoCloseable> (Note: <T implements AutoCloseable> is illegal syntax)

🟡 Middle Level

1. Enabling Method Invocations on Generic Variables

Without bounds, a generic variable $T$ is treated as Object, restricting method calls to toString(), hashCode(), and equals(). Bounding $T$ unlocks the API surface of the bounding type:

// Compile error without bound: Operator '>' cannot be applied to 'T, T'
// public static <T> int compare(T a, T b) { return a.compareTo(b); } 

// Valid with bound: compiler verifies that compareTo() is available
public static <T extends Comparable<T>> int compare(T a, T b) {
    return a.compareTo(b);
}

2. Multiple Bounds (Intersection Types)

A single type parameter can be constrained by multiple bounds using the ampersand (&) intersection operator:

public class FinancialProcessor<T extends Number & Comparable<T> & Serializable> {
    private final T amount;

    public FinancialProcessor(T amount) {
        this.amount = amount;
    }

    public boolean isPositive() {
        return amount.doubleValue() > 0.0; // From Number
    }

    public boolean isGreaterThan(T other) {
        return amount.compareTo(other) > 0; // From Comparable
    }
}

Ordering Constraint (JLS §4.4)

If one of the bounds is a class, it must be listed first:

// Valid: Class first, interfaces follow
public class Valid<T extends Number & Serializable> {}

// Compilation Error: Interface cannot precede a class in bounds
// public class Invalid<T extends Serializable & Number> {}

Because Java does not support multiple class inheritance, a type parameter can have at most one class bound.


🔴 Senior Level

The First Bound Rule in Type Erasure (JLS §4.6)

When compiling generic classes with multiple bounds, the Java compiler erases the type variable $T$ to its first bound:

public class Processor<T extends CharSequence & Serializable> {
    private T payload;
    public T getPayload() { return payload; }
}

Bytecode Transformation:

  1. The field payload erases to java.lang.CharSequence (the first declared bound).
  2. At call sites where payload is assigned or passed to a context requiring Serializable, the compiler automatically emits a synthetic checkcast instruction to java.io.Serializable.
  3. This is why a class bound must always be listed first: JVM instance fields and method descriptors require a single primary superclass descriptor in the class constant pool.

The Absolute Prohibition of super on Type Parameters

A common trick question on technical interviews asks whether you can declare a lower bound on a type parameter:

// ❌ COMPILATION ERROR: Syntax error, 'super' not allowed here
public class LowerBoundBox<T super Integer> {}

Why Lower Bounds on Type Parameters Are Prohibited:

  1. Ambiguous API Surface: An upper bound <T extends Number> guarantees that $T$ possesses at least the methods of Number. A lower bound <T super Integer> would mean $T$ is Integer, Number, or Object. The compiler could only guarantee methods from Object, rendering the bound useless for method dispatch.
  2. Type Erasure Breakdown: If $T$ could be Integer, Number, or Object, what type should the compiler erase $T$ to in bytecode? Erasing to Object discards all constraints; erasing to Integer violates covariance.
  3. Language Scope: The super keyword is strictly reserved for wildcards (<? super Integer>), which operate as anonymous consumption parameters rather than named type declarations.

Recursive Type Bounds and Inheritance Hierarchies

A Recursive Type Bound occurs when a type parameter is constrained by an expression containing itself:

// Standard JDK definition: java.lang.Enum
public abstract class Enum<E extends Enum<E>> implements Comparable<E> { ... }

The Pitfall with Subclassing:

Consider finding the maximum element in a collection:

// Naive implementation:
public static <T extends Comparable<T>> T maxNaive(Collection<T> coll) { ... }

Now consider an inheritance hierarchy:

public class Task implements Comparable<Task> { ... }
public class PriorityTask extends Task { ... }

PriorityTask does not implement Comparable<PriorityTask>; it implements Comparable<Task> via its superclass. Invoking maxNaive(priorityTasks) fails compilation because PriorityTask does not implement Comparable<PriorityTask>.

Production Solution (Relaxed Recursive Bound):

Using Josh Bloch’s PECS rule, declare the bound with a wildcard:

public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
    Iterator<T> it = coll.iterator();
    T max = it.next();
    while (it.hasNext()) {
        T next = it.next();
        if (next.compareTo(max) > 0) {
            max = next;
        }
    }
    return max;
}

Now, max(priorityTasks) compiles cleanly because PriorityTask implements Comparable<? super PriorityTask> (specifically, Comparable<Task>).


4 Tricky Questions

1. Can you declare a lower bound for a type parameter, such as <T super Number>?

Answer: No, it is a syntax error in Java. The Java Language Specification (JLS §4.4) prohibits the super keyword in type parameter declarations. Lower bounds are permitted exclusively on wildcards (<? super Number>).

A type parameter <T> defines a type variable that represents a concrete, named type used to define fields, local variables, and return types. Because a lower bound specifies a floor rather than a ceiling, the compiler cannot determine what methods exist on $T$ (other than Object), and cannot determine what type $T$ should be erased to in bytecode.

2. Why must a class bound appear first in multiple bounds (<T extends ClassA & InterfaceB>), and what happens if you swap them?

Answer: Swapping them (<T extends InterfaceB & ClassA>) causes a compilation error: "Interface expected here" or "The class must be the first bound".

This is required by both the Java type system and JVM bytecode architecture:

  1. In Java’s single-inheritance model, a class can have only one superclass.
  2. Under JLS §4.6, the compiler erases a type parameter to its first bound. For the JVM to emit valid class definitions and method descriptors, the primary class type descriptor must occupy that first bound slot.

3. What is the fundamental architectural difference between a Bounded Type Parameter (<T extends Number> void process(T val)) and a Bounded Wildcard (void process(List<? extends Number> list))?

Answer:

  • Bounded Type Parameter (<T extends Number>): Introduces a named type variable $T$. It allows binding relationships across multiple arguments (e.g., void copy(T src, T dest) guarantees both are the exact same subtype of Number) and typing the return value (T calculate()).
  • Bounded Wildcard (<? extends Number>): Represents an anonymous unknown subtype. It cannot bind multiple arguments together, cannot be used as a return type, and cannot be used to declare local variables. Wildcards are designed specifically for flexible collection read/write variance (PECS).

4. Why does Collections.max declare its signature as <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) in modern JDKs?

Answer: This signature incorporates two senior-level generic designs:

  1. Comparable<? super T>: Allows subclasses (e.g., ScheduledTask extends Task implements Comparable<Task>) to be compared using their superclass’s comparator logic without compilation failure.
  2. T extends Object & ... (Binary Compatibility Hack): Prior to Java 5, Collections.max returned Object. In Java 5, when genericizing the JDK, the compiler would have erased <T extends Comparable> to Comparable, changing the bytecode return descriptor from ()Ljava/lang/Object; to ()Ljava/lang/Comparable;, breaking binary compatibility with pre-Java 5 binaries. Adding Object & forced the first bound to remain Object, preserving 100% binary compatibility while offering full compile-time type safety.

🎯 Interview Cheat Sheet

  • Core Syntax: <T extends Bound>. Keyword is always extends, even for interfaces.
  • Dual Purpose: Restricts acceptable types at compile time and exposes methods of the bound on generic variables.
  • Multiple Bounds: <T extends ClassBound & Interface1 & Interface2>.
    • Max 1 class allowed.
    • Class bound must be listed first.
  • First Bound Erasure: Bytecode erases $T$ to its first bound; references to subsequent bounds trigger synthetic checkcast instructions.
  • Prohibition: <T super LowerBound> is illegal syntax; super is only valid for wildcards (<? super LowerBound>).
  • PECS for Sorting: Always prefer <T extends Comparable<? super T>> over naive <T extends Comparable<T>> to support polymorphic class hierarchies.
  • Binary Compatibility Idiom: <T extends Object & Interface> forces erasure to Object to preserve pre-generics method descriptors.