JAVA / GENERICS AND TYPE ERASURE
Bounded types for constraining what fits
Constrain a type parameter with extends, &, and self-referential bounds so generic code can call real methods on T and still reject bad type arguments.
What you will learn
- Declare <T extends Weighted> so the bound's methods are callable on T inside the class
- Stack requirements with &, class bound first: <T extends Number & Comparable<T>>
- Use self-referential bounds like <T extends Enum<T>> to keep T in return types
- Remember a bounded T erases to its leftmost bound, not to Object
Understanding Bounded types for constraining what fits
A type parameter with no bound is implicitly <T extends Object>, so inside the class the only methods you can call on a T value are Object's: equals, hashCode, toString. That is why unbounded generic code can store, pass around and return values but never inspect them. Writing <T extends Comparable<T>> raises that floor, because the compiler now treats every T value as at least a Comparable<T>, so item.compareTo(best) type-checks. The keyword is always extends, even when the bound is an interface; a type parameter list has no implements.
A bound works in two directions at once. Inside the generic code it is a permission: you may use everything the bound declares. At the call site it is a restriction, so Box<Object> is rejected because Object has no compareTo, and the error lands on the line that made the bad choice rather than somewhere inside the class body. The reason to bound a parameter instead of simply declaring fields of the bound type is exactness: Pallet<Crate> still hands back List<Crate>, whereas a class holding List<Weighted> forces every caller to cast on the way out.
Bounds compose with &, and the list is conjunctive, so <T extends Number & Comparable<T>> demands both at once. At most one bound may be a class and it must be written first; every bound after it must be an interface. A bound may also mention T itself, as in T extends Comparable<T> or T extends Enum<T>, which is how you say that a type interacts with its own kind rather than with the erased supertype. One consequence worth carrying forward: a bounded T erases to its leftmost bound instead of Object, so the order of the bounds is part of the compiled signature.
import java.util.ArrayList;
import java.util.List;
class Box<T extends Comparable<T>> {
private final List<T> items = new ArrayList<>();
void add(T item) {
items.add(item);
}
T max() {
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) { // legal only because of the bound
best = item;
}
}
return best;
}
}
public class Main {
public static void main(String[] args) {
Box<String> words = new Box<>();
words.add("pear");
words.add("fig");
words.add("quince");
System.out.println(words.max());
Box<Integer> nums = new Box<>();
nums.add(7);
nums.add(42);
nums.add(13);
System.out.println(nums.max());
// Box<Object> would not compile: Object has no compareTo
}
}
A bound is simultaneously a permission the generic code can rely on and a filter on which type arguments callers are allowed to supply.
Worked examples
Two bounds joined with &
Requiring both a class and an interface so the method can do arithmetic and comparison on the same values.
import java.util.List;
public class Main {
static <T extends Number & Comparable<T>> String describe(List<T> values) {
T smallest = values.get(0);
double total = 0;
for (T v : values) {
if (v.compareTo(smallest) < 0) {
smallest = v;
}
total += v.doubleValue();
}
return "min=" + smallest + " sum=" + total;
}
public static void main(String[] args) {
System.out.println(describe(List.of(9, 4, 7)));
System.out.println(describe(List.of(2.5, -1.5)));
}
}
Example explained
Line 1Number is a class, so it must be the first bound; everything after & has to be an interface.
Line 2v.doubleValue() type-checks because of the Number bound and v.compareTo(smallest) because of the Comparable<T> bound.
Line 3T is inferred as Integer on the first call and Double on the second, and both satisfy both bounds, so the body needs no cast.
Line 4T erases to Number, the leftmost bound, which is why the compiler must insert a cast to Comparable at the compareTo call.
A bound that mentions its own parameter
Using T extends Enum<T> so a generic helper can return T instead of a widened supertype.
public class Main {
enum Priority { LOW, MEDIUM, HIGH }
enum Color { RED, GREEN }
static <T extends Enum<T>> T next(T value) {
T[] all = value.getDeclaringClass().getEnumConstants();
return all[(value.ordinal() + 1) % all.length];
}
public static void main(String[] args) {
System.out.println(next(Priority.LOW));
System.out.println(next(Priority.HIGH));
System.out.println(next(Color.GREEN));
}
}
Example explained
Line 1T extends Enum<T> is self-referential: T must be an enum whose own constants are Ts, which is exactly how java.lang.Enum declares its parameter.
Line 2Because the bound carries T, getDeclaringClass() is typed Class<T> and getEnumConstants() returns T[], so no cast and no unchecked warning appear.
Line 3Drop the T from the bound and getEnumConstants() no longer yields T[], so returning an element would need a cast the compiler cannot verify.
Line 4getDeclaringClass() rather than getClass() is used because a constant with its own class body has an anonymous subclass as its runtime class.
Bounding keeps the element type exact
Comparing a bounded type parameter against simply storing the interface type.
import java.util.ArrayList;
import java.util.List;
interface Weighted {
double kilos();
}
class Crate implements Weighted {
private final String id;
private final double kilos;
Crate(String id, double kilos) {
this.id = id;
this.kilos = kilos;
}
public double kilos() {
return kilos;
}
@Override
public String toString() {
return id;
}
}
class Pallet<T extends Weighted> {
private final List<T> load = new ArrayList<>();
private final double limit;
Pallet(double limit) {
this.limit = limit;
}
boolean tryAdd(T item) {
if (total() + item.kilos() > limit) {
return false;
}
load.add(item);
return true;
}
double total() {
double sum = 0;
for (T item : load) {
sum += item.kilos();
}
return sum;
}
List<T> contents() {
return load;
}
}
public class Main {
public static void main(String[] args) {
Pallet<Crate> pallet = new Pallet<>(10.0);
System.out.println(pallet.tryAdd(new Crate("A", 6.0)));
System.out.println(pallet.tryAdd(new Crate("B", 5.0)));
System.out.println(pallet.tryAdd(new Crate("C", 4.0)));
Crate first = pallet.contents().get(0);
System.out.println(pallet.contents() + " " + pallet.total() + " " + first);
}
}
Example explained
Line 1The bound lets tryAdd call item.kilos() while the parameter type stays T, so a Pallet<Crate> accepts only Crate values.
Line 2Crate first = pallet.contents().get(0) needs no cast; a class that stored List<Weighted> would hand back Weighted and force one.
Line 3Crate B is refused because 6.0 + 5.0 exceeds the 10.0 limit, while C fits exactly at 10.0 and is accepted.
Line 4Writing Pallet<String> fails to compile at that declaration, because String does not implement Weighted, so the check never reaches the class body.
Important notes
Type parameters take upper bounds only; there is no <T super Number>, because super belongs to wildcards, which are a separate mechanism.
Reordering interface bounds is not cosmetic: <T extends Serializable & CharSequence> erases T to Serializable while <T extends CharSequence & Serializable> erases it to CharSequence, changing method signatures already compiled against.
Common mistakes
Writing <T implements Comparable<T>>: the type parameter grammar has no implements, so the file does not compile at all; extends covers interfaces here.
Using <T extends Comparable<T>> over a hierarchy: if Manager extends Employee and only Employee implements Comparable<Employee>, Manager fails the bound and the call is rejected, while <T extends Comparable<? super T>> accepts it.
Expecting the bound to supply a constructor or an array type: new T() and new T[8] still do not compile, because the bound restricts T at compile time and no concrete class exists at runtime, so pass a factory or use the bound's own methods.
Try it yourself
Change, predict, then run
Write static <T extends CharSequence> T longest(List<T> items) that returns the item with the greatest length(), and call it with a List<String> and a List<StringBuilder>. Then call it with a List<Integer> and read the error the bound produces.
Open the Java workspaceCheck your understanding
In static <T extends Number & Comparable<T>> T smallest(List<T> xs), what does the & Comparable<T> part actually change compared with <T extends Number>?
- It changes the erasure of T from Number to Comparable, so the compiled signature differs
- It lets callers pass either a Number or a Comparable, whichever they happen to have
- It makes compareTo callable on T inside the method and rules out Number subtypes that do not compare to themselves, such as AtomicInteger
- It is documentation only, since the compiler enforces just the first bound in the list
Show answer
Bounds joined by & are conjunctive, so a legal type argument must satisfy every one of them; that is what makes compareTo type-check inside the body and what excludes AtomicInteger, a Number with no compareTo. The erasure option is tempting because bounds do affect erasure, but erasure always takes the leftmost bound, so T still erases to Number.