What are bounded type parameters
You specify that the type must extend a certain class or implement an interface.
π’ 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 onlyT 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
- 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()forT 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]]