Week 5 Day 2 — Bounds and Generic Methods
Goal
Today I want bounds: restricting T so I can call methods on it, not only store it.
Main questions:
- What is an upper bound (
extends)? - What is a multiple bound?
- How do generic methods use bounds?
- What is
superon a type parameter? (preview of wildcards) - How do Spring APIs use bounds?
1. Unbounded T is Object
Inside Box<T>, the compiler treats T as Object. I can call toString and equals. I cannot call intValue() or save().
To call more, I bound T:
public class IdBox<T extends Number> {
private final T id;
public IdBox(T id) {
this.id = Objects.requireNonNull(id);
}
public int intId() {
return id.intValue(); // legal: T is a Number
}
}
new IdBox<>(42); // Integer
new IdBox<>(42L); // Long
// new IdBox<>("x"); // does not compile
T extends Number is an upper bound: T is Number or a subtype. After erasure, T becomes Number, not Object (Day 3).
Memory sentence:
T extends Numbermeans I can useTas aNumber. Callers must pass a Number subtype.
2. Multiple bounds
public static <T extends Number & Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
T must be a Number and Comparable<T>. The class bound, if any, comes first; then interfaces. There is at most one class.
This shows up when I need both “is a number” and “can be ordered.”
3. Generic methods with bounds
public static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) {
best = item;
}
}
return best;
}
Comparable<T> is itself generic. String implements Comparable<String>. A sloppy Comparable without a parameter is a raw type.
Self-bounded types (T extends Comparable<T>) mean “comparable to my own type,” which is what max wants. Week 6 will use the same idea for Comparable.
Inference:
Integer m = max(List.of(1, 3, 2)); // T = Integer
If inference fails, I can help: ClassName.<Integer>max(...).
4. Recurring type parameters in APIs
<S extends T> S save(S entity);
Spring Data’s save is not T save(T entity) only. S extends T lets me save a subtype and get that subtype back without a cast:
VipOrder vip = repository.save(new VipOrder(...));
Converter<S, T> names two independent parameters: source and target.
public interface Converter<S, T> {
T convert(S source);
}
Bounds here are often omitted; the names document the roles.
5. Spring connection
JpaRepository<T, ID extends Serializable>(historically; Boot 3 usesIDwithout forcingSerializablein the same way — I still useLong/UUID).ResponseEntity<T>is unbounded; I fillTwith the body type.RestClient/ParameterizedTypeReference<T>needs a subclass withTfilled in because bounds and parameters vanish at runtime (tomorrow).- Validation and conversion:
Converter<String, Money>is a bounded-by-convention pair of types.
6. Common traps
Trap 1: T extends Number and then new T(). Still illegal. Bounds do not bring back constructors.
Trap 2: class Foo<T extends Object> — redundant. Unbounded T already means Object.
Trap 3: Comparable without <T> inside a bound. Raw type, warnings, weak compareTo(Object).
Trap 4: Confusing a bound on T with a wildcard on a use site (List<? extends Number>). Bounds declare the class. Wildcards are Day 4.
Practice Questions and Answers
Question 1
What does T extends Number allow inside the class?
Answer:
I can call Number methods on values of type T. Callers may pass Integer, Long, BigDecimal, not String. After erasure, T is Number.
Question 2
Why write <T extends Comparable<T>> on a max method?
Answer:
I need to call compareTo on the elements, and I want them comparable to their own type. Without the bound, T is Object and compareTo does not exist.
Question 3
Can a bound list two classes?
Answer:
No. At most one class, and it must be first, then interfaces: T extends Entity & Comparable<T>. Java has single class inheritance.
Question 4
Why does Spring Data use <S extends T> S save(S entity)?
Answer:
So I can pass a subtype of the entity and get that subtype back. T save(T) would return the base type and force a cast.
Question 5
Does a bound let me write new T()?
Answer:
No. The compiler still does not know which constructor exists. Factories, Supplier<T>, or Class<T> plus reflection are the workarounds, and they are ugly. Prefer passing an instance or a factory.
Memory sentences
Bound
Tso I can call methods on it. UnboundedTisObject.
One class bound first, then interfaces.
<S extends T> S save(S)returns the subtype I passed in.