Question 15 Β· Section 20

What are bounded type parameters

You specify that the type must extend a certain class or implement an interface.

Language versions: English Russian Ukrainian

🟒 Junior Level

Bounded type parameters are a way to restrict the types that can be used with a generic.

You specify that the type must extend a certain class or implement an interface.

// Without restriction β€” any type
public class Box<T> { }
Box<String> box1 = new Box<>();  // OK
Box<Integer> box2 = new Box<>();  // OK

// With restriction β€” only Number and subclasses
public class NumberBox<T extends Number> { }
NumberBox<Integer> box1 = new NumberBox<>();  // βœ… OK
NumberBox<Double> box2 = new NumberBox<>();   // βœ… OK
NumberBox<String> box3 = new NumberBox<>();   // ❌ Error!

Syntax:

  • T extends Class β€” this class and its subclasses only
  • T extends Interface β€” only classes implementing the interface

🟑 Middle Level

How it works

Single bound:

// Only Comparable types
public class SortedList<T extends Comparable<T>> {
    private List<T> list = new ArrayList<>();

    public void add(T item) {
        list.add(item);
        list.sort(Comparable::compareTo);  // works β€” T is definitely Comparable
    }
}

SortedList<Integer> ints = new SortedList<>();  // βœ… Integer implements Comparable
SortedList<String> strings = new SortedList<>(); // βœ… String implements Comparable
SortedList<Object> objs = new SortedList<>();    // ❌ Object does not implement Comparable

Why it’s needed:

// Without bound β€” cannot call methods
public class Box<T> {
    public void process(T item) {
        // item.compareTo(other);  // cannot β€” T can be anything
    }
}

// With bound β€” can call Number methods
public class Stats<T extends Number> {
    private List<T> numbers;

    public double average() {
        return numbers.stream()
            .mapToDouble(Number::doubleValue)  // βœ… works!
            .average().orElse(0);
    }
}

Multiple bounds

You can specify multiple constraints:

// T must be Serializable AND Comparable
public class DataBox<T extends Comparable<T> & Serializable> {
    private T value;

    public int compare(T other) {
        return value.compareTo(other);  // βœ…
    }

    public byte[] serialize() {
        // βœ… can serialize β€” T extends Serializable
        return serializeValue(value);
    }

    private static byte[] serializeValue(Serializable value) {
        // standard Java serialization
        try (var baos = new java.io.ByteArrayOutputStream();
             var oos = new java.io.ObjectOutputStream(baos)) {
            oos.writeObject(value);
            return baos.toByteArray();
        } catch (java.io.IOException e) {
            throw new RuntimeException(e);
        }
    }
}

DataBox<String> box = new DataBox<>();  // βœ… String implements both
DataBox<Integer> box2 = new DataBox<>(); // βœ… Integer implements both

Common mistakes

  1. Wrong order: ```java // ❌ Class must come first public class Bad<T extends Serializable & Number> {} // error // JLS requires: class (if any) must be first, because erasure uses // the first bound. Java has single inheritance β€” only one class is allowed.

// βœ… Class first, then interfaces public class Good<T extends Number & Serializable> {} // OK


2. **Non-existent bound:**
```java
// ❌ Integer is a final class, cannot be a bound for inheritance
public class Box<T extends Integer> {}  // technically OK but useless

πŸ”΄ Senior Level

Internal Implementation

Type erasure with bounds:

public class Stats<T extends Number> {
    private T value;
    public double doubleValue() { return value.doubleValue(); }
}

// After erasure: T -> Number (first bound)
public class Stats {
    private Number value;
    public double doubleValue() { return value.doubleValue(); }
}

Multiple bounds erasure:

public class Box<T extends Comparable<T> & Serializable> { }

// Erasure: T -> first bound (Comparable)
public class Box {
    private Comparable value;
}

Architectural Trade-offs

Bounded vs unbounded:

Bounded Unbounded
Can call methods Cannot call methods
Type-safe Less type-safe
Limited flexibility Maximum flexibility

Edge Cases

1. Recursive bounds:

// T must be Comparable to itself
public class Enum<E extends Enum<E>> implements Comparable<E> {
    // Java enum internal
}

// Self-bounding types
public class SelfBounded<T extends SelfBounded<T>> {
    private T self;

    public T set(T self) {
        this.self = self;
        return self;
    }
}

2. Wildcard vs bounded parameter:

// Bounded type parameter β€” for classes/methods
public class NumberBox<T extends Number> { }

// Bounded wildcard β€” for parameters
public void process(List<? extends Number> numbers) { }

// Difference:
// - <T extends Number> β€” you can use T in methods
// - <? extends Number> β€” cannot create new elements

3. Bridge methods with bounds:

public class Node<T extends Comparable<T>> {
    private T data;
    public T getData() { return data; }
    public void setData(T data) { this.data = data; }
}

public class DateNode extends Node<Date> {
    @Override
    public Date getData() { return super.getData(); }
    @Override
    public void setData(Date data) { super.setData(data); }

    // Bridge method after erasure:
    // public void setData(Comparable data) { setData((Date) data); }
}

Performance

Bounded types:
- Runtime: Zero overhead
- Compile time: additional type checking
- Type erasure: bound becomes the type after compilation

Bounded vs unbounded:
- Same at runtime
- Bounded gives more information to the compiler
// After type erasure, the bound becomes a compile-time check.
// JIT sees no difference between bounded and unbounded β€” bytecode is identical.
// Bounds are only useful for compile-time type safety.

Production Experience

Generic Repository:

public interface BaseEntity<ID extends Serializable> {
    ID getId();
}

public abstract class JpaRepository<T extends BaseEntity<ID>, ID extends Serializable> {
    private final Class<T> entityType;

    public Optional<T> findById(ID id) {
        // type-safe findById
        return entityManager.find(entityType, id);
    }

    public <S extends T> S save(S entity) {
        if (entity.getId() == null) {
            return persist(entity);
        } else {
            return merge(entity);
        }
    }
}

Builder pattern with bounds:

public abstract class Builder<T, B extends Builder<T, B>> {
    protected abstract B self();

    public B withName(String name) {
        // ... setup
        return self();
    }

    public T build() {
        // ... build
    }
}

public class UserBuilder extends Builder<User, UserBuilder> {
    @Override
    protected UserBuilder self() { return this; }
}

User user = new UserBuilder()
    .withName("John")
    .build();

Best Practices

// βœ… Bounded for calling methods
public class Stats<T extends Number> {
    public double average() { /* use Number::doubleValue */ }
}

// βœ… Multiple bounds for multiple contracts
public class DataBox<T extends Comparable<T> & Serializable> {}

// βœ… Recursive bounds for self-types
public class Enum<E extends Enum<E>> {}

// ❌ Overly strict bounds
// ❌ Bounds on raw types

🎯 Interview Cheat Sheet

Must know:

  • Bounded type parameters: <T extends Number> β€” only Number and subclasses
  • Allows calling bound-type methods: value.doubleValue() for T extends Number
  • Multiple bounds: <T extends Comparable<T> & Serializable> β€” class first, then interfaces
  • Type erasure uses first bound: T extends Number & Serializable -> erasure = Number
  • Recursive bounds: <E extends Enum<E>> β€” type compared with itself
  • JLS requires: class (if any) must be first in the bounds list

Frequent follow-up questions:

  • Why are bounded types needed? β€” To call bound-type methods inside a generic class
  • Why must class be first in multiple bounds? β€” Erasure uses the first bound, single inheritance
  • What is a recursive bound? β€” T extends Comparable<T> β€” type compared with the same type
  • Relaxed recursive bound? β€” T extends Comparable<? super T> β€” allows subclasses to work too

Red flags (DO NOT say):

  • ❌ β€œYou can put interface before class in bounds” β€” JLS requires class first
  • ❌ β€œMultiple bounds give runtime overhead” β€” Zero overhead, bound is compile-time only
  • ❌ β€œBounded and unbounded are the same at runtime” β€” Same after erasure, but bounds give more compile-time info
  • ❌ β€œYou can have multiple classes in bounds” β€” Java single inheritance, maximum one class

Related topics:

  • [[11. What are Generics in Java]]
  • [[13. What is type erasure]]
  • [[16. What is the difference between <? extends T> and <? super T>]]
  • [[23. What is recursive type bound]]
  • [[26. Can you use multiple bounds for a single type parameter]]