Bounded Type Parameters in Java: extends and super Explained

Illustration for Bounded Type Parameters in Java: extends and super Explained
By Last updated:

Generics in Java are designed to provide type safety, reusability, and maintainability. Among their most powerful features are bounded type parameters, which allow developers to constrain the types that can be used in generic classes or methods. This ensures flexibility without sacrificing safety.

Bounded type parameters rely on the keywords extends and super, which define upper bounds and lower bounds. By mastering these, developers can write cleaner, more reusable APIs that adapt to different type hierarchies while enforcing meaningful constraints.

Think of bounded type parameters as blueprint molds with restrictions. While generics are molds that can take any material, bounded type parameters say, “you can only pour materials of a certain type or hierarchy into this mold.”


Core Definition and Purpose of Java Generics

Generics allow classes, interfaces, and methods to operate on type parameters instead of specific types. This provides:

  1. Type Safety – Prevents invalid types at compile time.
  2. Reusability – Enables one design to work across many data types.
  3. Maintainability – Cleaner, easier-to-read APIs.

Introduction to Type Parameters: <T>, <K, V>, <E>

  • <T>: General type parameter (Type).
  • <E>: Represents an element, often used in collections.
  • <K, V>: Used in key-value mappings.

Example:

class Box<T> {
    private T content;
    public void set(T content) { this.content = content; }
    public T get() { return content; }
}

Generic Classes with Examples

class Pair<K, V> {
    private K key; private V value;
    public Pair(K key, V value) { this.key = key; this.value = value; }
    public K getKey() { return key; }
    public V getValue() { return value; }
}

Usage:

Pair<String, Integer> age = new Pair<>("Age", 30);
System.out.println(age.getKey() + " = " + age.getValue());

Generic Methods: Declaration and Usage

public static <T> void printArray(T[] array) {
    for (T element : array) {
        System.out.println(element);
    }
}

Bounded Type Parameters: extends and super

Upper Bounds with extends

<T extends Class> means T can be Class or its subtype.

public static <T extends Number> double square(T number) {
    return number.doubleValue() * number.doubleValue();
}
  • Ensures only numeric types are allowed.
  • Prevents unrelated types like String from being passed.

Lower Bounds with super

<? super T> means the parameter can be of type T or any of its supertypes.

public static void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}
  • Ensures consumers can safely add items without breaking type safety.

Wildcards Explained: ?, ? extends, ? super

  • ? – Unknown type.
  • ? extends T – Upper bound (read-only, producer).
  • ? super T – Lower bound (write-only, consumer).

PECS Principle: Producer Extends, Consumer Super.

public static void processNumbers(List<? extends Number> list) { // producer
    for (Number n : list) {
        System.out.println(n);
    }
}

public static void insertNumbers(List<? super Integer> list) { // consumer
    list.add(100);
}

Multiple Type Parameters and Nested Generics

class Triple<A, B, C> {
    private A first; private B second; private C third;
}

Example: Map<String, List<Integer>>.


Generics in Collections Framework

  • List<E>List<String>
  • Map<K, V>Map<String, Integer>
  • Set<E>Set<Double>

Collections heavily use bounded type parameters.


Raw Types vs Parameterized Types

List list = new ArrayList(); // Raw type - unsafe
List<String> safe = new ArrayList<>(); // Parameterized type

Raw types bypass type checks and should be avoided.


Type Inference and Diamond Operator

Introduced in Java 7:

Map<String, Integer> map = new HashMap<>();

Type Erasure in Java

Generics are implemented with type erasure.
At runtime, type parameters are erased to their bounds (or Object).

This explains why you cannot create new T().


Reifiable vs Non-Reifiable Types

  • Reifiable Types – Fully known at runtime (List<?>).
  • Non-Reifiable Types – Lose type info after erasure (List<String>).

Recursive Type Bounds

class ComparableBox<T extends Comparable<T>> { ... }

Useful for defining self-referential hierarchies.


Fluent APIs and Builders with Generics

class Builder<T extends Builder<T>> {
    public T withName(String name) { return (T) this; }
}

Generics with Enums and Annotations

Example: EnumSet<E extends Enum<E>>.
Annotations like @SuppressWarnings("unchecked") apply to generics.


Generics with Exceptions

  • Cannot declare catch (T e).
  • Cannot instantiate new T().

Generics and Reflection

Reflection can retrieve generic info via ParameterizedType.


Case Studies

Type-Safe Cache

class Cache<K, V> {
    private Map<K, V> map = new HashMap<>();
    public void put(K key, V value) { map.put(key, value); }
    public V get(K key) { return map.get(key); }
}

Flexible Repository Pattern

interface Repository<T, ID> {
    void save(T entity);
    T findById(ID id);
}

Event Handling System

interface EventListener<T> {
    void onEvent(T event);
}

Best Practices for Bounded Type Parameters

  • Use upper bounds (extends) for read-only scenarios.
  • Use lower bounds (super) for write-only scenarios.
  • Follow PECS strictly.
  • Avoid unnecessary wildcards.

Common Anti-Patterns

  • Overly complex nested generics.
  • Misuse of wildcards.
  • Using raw types.

Performance Considerations

Bounded type parameters do not affect runtime performance since generics use type erasure. Their power lies in compile-time safety.


📌 What's New in Java for Generics?

  • Java 5: Generics introduced.
  • Java 7: Diamond operator (<>).
  • Java 8: Streams and functional interfaces use generics.
  • Java 10: var integrates with generics.
  • Java 17+: Sealed classes interact with generics.
  • Java 21: Virtual threads make concurrent generic APIs more scalable.

Conclusion and Key Takeaways

Bounded type parameters (extends and super) give developers fine-grained control over how generics interact with type hierarchies. By combining them with wildcards and following the PECS principle, you can design APIs that are flexible, reusable, and safe.


FAQ on Bounded Type Parameters in Java

Q1: What’s the difference between extends and super in generics?
extends sets an upper bound (subtypes allowed), super sets a lower bound (supertypes allowed).

Q2: Why can’t I create new T() in a generic class?
Because type information is erased at runtime.

Q3: How does PECS apply to bounded parameters?
Use extends when consuming data, super when producing data.

Q4: Can I use multiple bounds in generics?
Yes, e.g., <T extends Number & Comparable<T>>.

Q5: How do wildcards differ from bounded type parameters?
Wildcards (?) are used at usage sites, while bounds (extends, super) are applied at declaration.

Q6: Are bounded type parameters slower at runtime?
No, they compile down to type-erased bytecode.

Q7: Can I mix extends and super?
Yes, depending on whether the method consumes or produces data.

Q8: How does type erasure impact bounded parameters?
At runtime, bounded parameters are replaced with their bounds.

Q9: How are bounded parameters used in collections?
They allow flexible APIs, e.g., Collections.copy(List<? super T> dest, List<? extends T> src).

Q10: How do Spring and Hibernate use bounded parameters?
Repositories and DAOs often use extends to constrain entity types while keeping flexibility.