Zum Hauptinhalt springen

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:

  1. What is an upper bound (extends)?
  2. What is a multiple bound?
  3. How do generic methods use bounds?
  4. What is super on a type parameter? (preview of wildcards)
  5. 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 Number means I can use T as a Number. 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 uses ID without forcing Serializable in the same way — I still use Long / UUID).
  • ResponseEntity<T> is unbounded; I fill T with the body type.
  • RestClient / ParameterizedTypeReference<T> needs a subclass with T filled 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 T so I can call methods on it. Unbounded T is Object.

One class bound first, then interfaces.

<S extends T> S save(S) returns the subtype I passed in.

Next: Week 5 Day 3 — Type Erasure