6. Generics
Generics let a class or method be written once and used safely with many types, with the compiler checking every use. This chapter covers how to declare and consume generic types, how wildcards express "some unknown type" precisely enough to be useful, and why type erasure, the compromise that let generics ship without breaking existing code, produces the surprising restrictions you will run into. Everything in the Collections Framework and the Stream API is built on these rules, so this is the chapter that makes the next two readable.
<a id="61-generic-types"></a>
6.1 Generic Types
This subchapter covers declaring type parameters on classes and methods, and constraining what may be substituted for them.
Motivation and Type Safety
Before Java 5 there was no way to tell a collection what it was supposed to
hold. Every container stored Object, so anything could go in, and everything
came out needing a cast.
List names = new ArrayList(); // pre-generics: a list of Object
names.add("Ada");
names.add("Grace");
names.add(42); // nothing stops this: it is an Object too
String first = (String) names.get(0); // cast required on every read
String third = (String) names.get(2); // ClassCastException at RUNTIME
Two problems, and the second is the serious one. The casts are noise, but the
ClassCastException is a runtime failure caused by a mistake made much
earlier, possibly in a different file written by a different person. The
compiler had all the information needed to reject names.add(42) and was not
allowed to use it.
Generics give the container a type parameter, so the intent is part of the type:
List<String> names = new ArrayList<>(); // a list of String, and only String
names.add("Ada");
// names.add(42); // ERROR: caught at COMPILE time
String first = names.get(0); // no cast: the compiler knows the type
The shift is from detecting errors late and expensively to detecting them early and cheaply:
WITHOUT GENERICS WITH GENERICS
write code write code
| |
compile (no complaint) compile -> ERROR here, at your desk
|
ship
|
run -> ClassCastException
in production, 3am
A useful analogy: a pre-generics collection is an unlabelled cardboard box. You
can put anything in it, and when you take something out you have to guess what
it is and hope. A generic collection is a box stamped STRING ONLY, and the
loading dock refuses anything else.
Generics deliver three concrete benefits:
| Benefit | What it means |
|---|---|
| Compile-time checking | Type errors surface at build time, not in production |
| No casts | The compiler knows the element type already |
| Self-documenting APIs | Map<String, List<Order>> states its shape exactly |
The cost is paid at compile time only. Generics are erased before the bytecode runs, which is covered in 6.2, so there is no runtime performance penalty for using them.
A closing warning about raw types, which is what a generic type looks like with its parameters omitted. They still compile, for backwards compatibility, but they switch the checking off:
List<String> safe = new ArrayList<>();
List raw = safe; // legal, but warns: raw type
raw.add(42); // compiles! the checking is gone
String boom = safe.get(0); // ClassCastException: 42 is not a String
Never write a raw type in new code. The compiler's unchecked warning is telling you it has stopped protecting that reference.
Key Takeaways
- Without generics, collections held
Object, requiring casts and deferring type errors to runtime.- A type parameter moves those errors to compile time, where they are cheap to fix.
- Generic code needs no casts, because the compiler already knows the element type.
- Generic type declarations document an API's shape precisely.
- Raw types disable all of this and should never appear in new code.
🧪 Practice
- Write a raw
ArrayList, add aStringand anInteger, and trigger aClassCastExceptionon read.- Rewrite the same code with
List<String>and note at which stage the error now appears.- Interview: Generics add no runtime cost. What does that tell you about when the checking happens? (Hint: what is left in the bytecode after compilation?)
Generic Classes and Type Parameters
A generic class is a class with one or more placeholders for types, filled in by whoever uses it. The placeholder is declared in angle brackets after the class name and can then be used anywhere a real type could be: as a field type, a parameter, a return type, or a local variable.
Think of the class declaration as a template and the type argument as the
material: the same Box blueprint produces a box for strings and a box for
orders, with the compiler enforcing the difference.
public class Box<T> { // T is the type PARAMETER
private T contents; // a field of the parameterised type
public void put(T item) { this.contents = item; }
public T get() { return contents; } // no cast needed
public boolean isEmpty() { return contents == null; }
}
Box<String> stringBox = new Box<>(); // String is the type ARGUMENT
stringBox.put("hello");
String value = stringBox.get(); // typed as String automatically
// stringBox.put(42); // ERROR: incompatible types
Box<Order> orderBox = new Box<>(); // same class, different material
The distinction between the two words matters and is a common exam question:
| Term | Where it appears | Example |
|---|---|---|
| Type parameter | The class declaration | class Box<T> |
| Type argument | The use site | new Box<String>() |
| Parameterised type | The resulting type | Box<String> |
| Raw type | Parameters omitted | Box |
A class may declare several parameters, separated by commas:
public class Pair<K, V> { // two independent parameters
private final K key;
private final V value;
public Pair(K key, V value) { // the constructor uses them...
this.key = key; // ...but is NOT written Pair<K, V>(...)
this.value = value;
}
public K key() { return key; }
public V value() { return value; }
public Pair<V, K> swapped() { // reuses the parameters in a new order
return new Pair<>(value, key);
}
}
Pair<String, Integer> score = new Pair<>("Ada", 100);
Pair<Integer, String> flipped = score.swapped();
Note that the constructor is named Pair, not Pair<K, V>. The parameters are
already in scope from the class declaration.
Single-letter names are conventional and carry meaning:
| Letter | Conventional use |
|---|---|
T | Type, a general parameter |
E | Element, in a collection |
K | Key, in a map |
V | Value, in a map |
R | Result, a return type |
S, U | Second and third types |
Two limits to internalise now. A type argument must be a reference type, so primitives are not allowed and you use their wrapper classes instead:
// Box<int> intBox = new Box<>(); // ERROR: unexpected type
Box<Integer> intBox = new Box<>(); // the wrapper, with autoboxing
intBox.put(42); // autoboxed to Integer
And parameterised types are invariant: Box<String> is not a subtype of
Box<Object>, even though String is a subtype of Object. This surprises
everyone once, and wildcards in 6.2 exist to work around it.
// Box<Object> any = new Box<String>(); // ERROR: incompatible types
Key Takeaways
- A generic class declares type parameters in angle brackets after its name.
- A type parameter is the placeholder; a type argument is what you substitute at the use site.
- Constructors do not repeat the parameter list; it is already in scope.
- Type arguments must be reference types, so use wrappers rather than primitives.
Box<String>is not a subtype ofBox<Object>; generic types are invariant.
🧪 Practice
- Write a
Box<T>withputandget, then instantiate it forStringandInteger.- Extend
Pair<K, V>with atoStringand a static factory, and use it to return two values from a method.- Interview: Why can a type argument not be a primitive like
int? (Hint: what does the compiler substitute forTonce generics are erased?)
Generic Methods and Type Inference
A class-level parameter is fixed for the whole object. Sometimes only a single
method needs to be generic, and often it is a static method with no instance
to take a parameter from. A generic method declares its own type parameter,
scoped to that one method.
The declaration goes between the modifiers and the return type, which is the part that looks odd at first:
public static <T> void printAll(List<T> items) {
// ^^^ declares T for this method only
for (T item : items) System.out.println(item);
}
public class Utils {
// Returns the first element, or a fallback if the list is empty.
public static <T> T firstOr(List<T> items, T fallback) {
return items.isEmpty() ? fallback : items.get(0);
}
// Two independent parameters, used for input and output.
public static <T, R> List<R> mapAll(List<T> source, Function<T, R> mapper) {
List<R> result = new ArrayList<>();
for (T item : source) result.add(mapper.apply(item));
return result;
}
// Swaps two elements in place; T ties the array and indices together.
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
The payoff is type inference: you almost never write the type argument, because the compiler works it out from the arguments you passed.
List<String> names = List.of("Ada", "Grace");
String first = Utils.firstOr(names, "nobody"); // T inferred as String
List<Integer> lengths = Utils.mapAll(names, String::length); // T=String, R=Integer
// The explicit form exists but is rarely needed:
String same = Utils.<String>firstOr(names, "nobody");
Inference occasionally needs help. When there is nothing to infer from, such as
an empty collection or a null, the compiler falls back to Object and you
supply the argument explicitly:
List<String> empty = Collections.emptyList(); // inferred from the target
List<String> also = Collections.<String>emptyList(); // explicit, when needed
A generic method on a generic class may introduce a different parameter, and the two are unrelated:
public class Box<T> {
private T contents;
public <U> Pair<T, U> pairedWith(U other) { // U is new, T comes from the class
return new Pair<>(contents, other);
}
}
Box<String> box = new Box<>();
Pair<String, Integer> pair = box.pairedWith(42); // T=String, U=Integer
| Form | Parameter declared on | Scope | Inferred from |
|---|---|---|---|
| Generic class | The class | Every member | The new expression |
| Generic method | The method | That method | The call's arguments |
The practical rule: if only one method needs the parameter, put it on the method, not the class. A class-level parameter forces every user of the class to commit to a type they may not care about.
Key Takeaways
- A generic method declares its own type parameter before the return type.
- The parameter is scoped to that method and is independent of any class-level parameter.
- Type inference means the argument is almost never written explicitly.
- Supply the type argument explicitly when there is nothing to infer from, such as an empty collection.
- Prefer a method-level parameter when only one method is generic.
🧪 Practice
- Write a generic
<T> T last(List<T> list)and call it on lists of two different types.- Write
<K, V> Map<V, K> invert(Map<K, V> map)and test it.- Interview: When does the compiler need you to write the type argument explicitly? (Hint: what can it infer from a call with no useful arguments?)
The Diamond Operator
Early generic code repeated itself. The type arguments appeared on both sides of the assignment, and the right side never said anything the left side had not already established.
// Java 5 and 6: everything stated twice.
Map<String, List<Order>> byCustomer = new HashMap<String, List<Order>>();
Java 7 introduced the diamond operator, an empty <> that tells the
compiler to infer the arguments from the context:
Map<String, List<Order>> byCustomer = new HashMap<>(); // inferred
List<String> names = new ArrayList<>();
Box<Pair<String, Integer>> nested = new Box<>(); // scales with nesting
The diamond is not the same as omitting the brackets. Leaving them off creates a raw type and silently disables checking, which is the bug from the first topic:
List<String> a = new ArrayList<>(); // generic, inferred: correct
List<String> b = new ArrayList(); // RAW on the right: unchecked warning
That one-character difference is worth checking for in code review, because both compile.
Inference comes from the target type, meaning whatever the expression is assigned to or passed as:
List<String> fromVariable = new ArrayList<>(); // from the variable type
process(new ArrayList<>()); // from the parameter type
public static List<String> make() {
return new ArrayList<>(); // from the return type
}
// var reverses the direction, so the diamond has nothing to work from:
var list = new ArrayList<String>(); // must state it here
// var broken = new ArrayList<>(); // infers ArrayList<Object>: rarely wanted
That last case matters now that var is common. With var, the right side is
the only source of information, so write the type argument there.
Since Java 9 the diamond also works with anonymous classes, which was previously forbidden:
Comparator<String> byLength = new Comparator<>() { // Java 9+: diamond allowed
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
Key Takeaways
- The diamond
<>infers type arguments from the target type.- Omitting the brackets entirely creates a raw type and disables checking.
- Inference works from a variable type, a parameter type, or a return type.
- With
varthere is no target type, so the arguments must be written on the right.- Java 9 extended the diamond to anonymous classes.
🧪 Practice
- Rewrite three verbose generic declarations using the diamond.
- Write
List<String> x = new ArrayList();and read the compiler's unchecked warning.- Declare
var list = new ArrayList<>();and inspect the inferred element type; explain the result.
Bounded Type Parameters
An unbounded <T> accepts anything, which means the code inside can only use
the methods every object has. That is often too little. A method that sums
numbers needs doubleValue(); a method that sorts needs compareTo. Neither
is available on Object.
A bound constrains what may be substituted, and in exchange the compiler lets you call that type's methods.
// Without a bound: T could be anything, so nothing useful is available.
public static <T> double sumBroken(List<T> numbers) {
double total = 0;
for (T n : numbers) {
// total += n.doubleValue(); // ERROR: Object has no doubleValue()
}
return total;
}
// With an upper bound: T is guaranteed to be a Number or a subtype.
public static <T extends Number> double sum(List<T> numbers) {
// ^^^^^^^^^^^^^^^ the bound
double total = 0;
for (T n : numbers) total += n.doubleValue(); // now allowed
return total;
}
sum(List.of(1, 2, 3)); // Integer extends Number: fine
sum(List.of(1.5, 2.5)); // Double extends Number: fine
// sum(List.of("a", "b")); // ERROR: String is not within bound Number
The keyword is always extends, whether the bound is a class or an interface.
There is no implements in a type bound, which trips people up:
public static <T extends Comparable<T>> T max(List<T> items) {
// ^^^^^^^^^^^^^ an interface, still written 'extends'
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item; // available thanks to the bound
}
return best;
}
String longest = max(List.of("ab", "abcd", "abc")); // String is Comparable
The bound serves two purposes at once, and it is worth separating them:
| Purpose | Effect |
|---|---|
| Restriction | Rejects type arguments outside the bound, at compile time |
| Capability | Grants access to the bound type's members inside the body |
Bounds work on classes as well as methods, and a bound may refer to another parameter:
public class NumberBox<T extends Number> { // class-level bound
private final List<T> values = new ArrayList<>();
public void add(T value) { values.add(value); }
public double average() {
return values.stream().mapToDouble(Number::doubleValue).average().orElse(0);
}
}
// A bound referring to another type parameter:
public static <T, C extends Collection<T>> C fill(C target, T value, int times) {
for (int i = 0; i < times; i++) target.add(value);
return target; // returns the concrete collection type, not Collection
}
Note the difference between a bound and a wildcard, which 6.2 covers: a bound
constrains a parameter you declared, while a wildcard describes an unknown
type at a use site. <T extends Number> names the type so you can use it
consistently; List<? extends Number> does not.
Key Takeaways
- A bound restricts which types may be substituted and grants access to that type's members.
- Always written with
extends, for both superclasses and interfaces.- Without a bound, only
Objectmembers are available inside the generic code.- Bounds may be applied to classes as well as methods.
- A bound may reference another type parameter, as in
<T, C extends Collection<T>>.
🧪 Practice
- Write
<T extends Number> double average(List<T> values)and call it with integers and doubles.- Write a
<T extends Comparable<T>> T min(List<T>)method and test it on strings.- Interview: Why is
implementsnever used in a type bound? (Hint: does the compiler need to distinguish a class bound from an interface bound here?)
Multiple Bounds
Sometimes one constraint is not enough. A method that both sorts its input and
serialises the result needs a type that is Comparable and Serializable.
Java allows a type parameter to carry several bounds, joined by &.
public static <T extends Comparable<T> & Serializable> T maxSerializable(List<T> items) {
// ^^^^^^^^^^^^^ ^^^^^^^^^^^^ both required
T best = items.get(0);
for (T item : items) {
if (item.compareTo(best) > 0) best = item; // from Comparable
}
return best; // guaranteed Serializable
}
Only a type satisfying every bound may be substituted:
maxSerializable(List.of("a", "b")); // String is Comparable and Serializable
class Plain implements Comparable<Plain> {
@Override public int compareTo(Plain o) { return 0; }
}
// maxSerializable(List.of(new Plain())); // ERROR: Plain is not Serializable
One ordering rule governs the syntax: if a class is among the bounds, it must be listed first, and there may be at most one, since Java has single class inheritance. Interfaces follow in any order.
// Correct: class first, then interfaces.
public static <T extends Number & Comparable<T> & Serializable> void ok(T value) { }
// public static <T extends Comparable<T> & Number> void bad(T value) { }
// ERROR: interface expected here (Number is a class and must come first)
<T extends Number & Comparable<T> & Serializable>
^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^
class interface interface
(at most one, and always first)
Inside the method, every member of every bound is available, as though the type were a hypothetical class implementing all of them:
public static <T extends Number & Comparable<T>> String describe(T value, T other) {
double d = value.doubleValue(); // from Number
int cmp = value.compareTo(other); // from Comparable
return d + (cmp > 0 ? " is larger" : " is not larger");
}
Multiple bounds are uncommon in application code and more frequent in library APIs, where an author wants precision. Two cautions keep them usable. Each extra bound narrows the set of acceptable types, so three or four bounds often means the design should use a single purpose-built interface instead. And the leftmost bound determines the erasure, which matters for the binary compatibility rules covered in the next subchapter.
Key Takeaways
- Multiple bounds are written with
&and require the type to satisfy all of them.- At most one bound may be a class, and it must be listed first.
- Every member of every bound is available inside the generic code.
- Heavily bounded parameters usually signal a missing interface.
- The leftmost bound determines what the parameter erases to.
🧪 Practice
- Write a method bounded by
Comparable<T> & Serializableand call it withString.- Put an interface before a class in a bound list and read the compiler error.
- Interview: Why may only one bound be a class, and why must it come first? (Hint: connect it to a rule from the inheritance chapter.)
<a id="62-advanced-generics"></a>
6.2 Advanced Generics
Wildcards make generic APIs flexible enough to be usable, and erasure explains every restriction the language imposes on them.
Wildcards and Unknown Types
Generic types are invariant, a rule introduced in 6.1 that becomes a real
obstacle here. List<String> is not a List<Object>, so a method written to
accept List<Object> rejects every other list:
public static void printAll(List<Object> items) {
for (Object item : items) System.out.println(item);
}
printAll(new ArrayList<Object>()); // fine
// printAll(new ArrayList<String>()); // ERROR: incompatible types
The restriction is not arbitrary. If List<String> were assignable to
List<Object>, this would compile and then corrupt the list:
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // suppose this were allowed...
// objects.add(42); // ...then an Integer is in a List<String>
// String boom = strings.get(0); // ClassCastException, generics defeated
Arrays are covariant and do allow exactly that, which is why array stores are checked at runtime:
Object[] array = new String[2]; // legal: arrays are covariant
array[0] = 42; // compiles, then ArrayStoreException at runtime
Generics chose compile-time safety instead. The wildcard ? is how you
recover the flexibility without losing it. It means "some specific type I do
not know":
public static void printAll(List<?> items) { // a list of SOMETHING
for (Object item : items) System.out.println(item);
}
printAll(new ArrayList<String>()); // now all of these work
printAll(new ArrayList<Integer>());
printAll(List.of(1.5, 2.5));
Because the element type is unknown, the compiler blocks anything that would
require knowing it. You can read as Object, because every type is an
Object, but you cannot write anything:
public static void inspect(List<?> items) {
Object first = items.get(0); // OK: whatever it is, it is an Object
int size = items.size(); // OK: does not involve the element type
items.clear(); // OK: removes without naming the type
// items.add("hello"); // ERROR: ? might not be String
// items.add(new Object()); // ERROR: ? might be String
items.add(null); // the single exception: null fits any type
}
List<?> means "a List<X> for exactly one X, but I don't know which"
could be List<String> -> add(Integer) would be wrong
could be List<Integer> -> add(String) would be wrong
therefore: add(anything) is rejected, read as Object is safe
The distinction from a type parameter matters. <T> names the type, so it can
be used consistently in several places; ? does not, so each ? is
independent:
| Form | Names the type | Usable in several positions | Declared where |
|---|---|---|---|
<T> void f(List<T> a, List<T> b) | Yes | Yes, both lists share T | Method signature |
void f(List<?> a, List<?> b) | No | No, the two are unrelated | Use site only |
// These two lists must have the SAME element type:
public static <T> void copy(List<T> from, List<T> to) { to.addAll(from); }
// These two may have DIFFERENT element types:
public static void compare(List<?> a, List<?> b) { ... }
One more form exists, the unbounded wildcard versus the raw type, and they are not the same thing:
List<?> wild = new ArrayList<String>(); // checked: add is rejected
List raw = new ArrayList<String>(); // unchecked: add compiles, unsafely
Use List<?> when you genuinely do not care about the element type. Never use
the raw form.
Key Takeaways
- Generic types are invariant, so
List<String>is not aList<Object>.- Invariance prevents the corruption that array covariance allows at runtime.
- The wildcard
?means one specific but unknown type.- You may read from a
List<?>asObjectbut may not add anything exceptnull.List<?>is type-checked; a rawListis not.
🧪 Practice
- Write a method taking
List<?>that prints every element, and call it with three differently typed lists.- Try adding an element inside that method and explain the error.
- Interview: Why are arrays covariant but generics invariant? (Hint: one checks at runtime and one at compile time; what does each cost?)
Upper and Lower Bounded Wildcards
An unbounded ? is safe but nearly useless: you can only read Object and
write nothing. Bounded wildcards restore precision by constraining the unknown
type from above or below.
An upper bounded wildcard, ? extends T, means "some subtype of T". You
know every element is at least a T, so reading as T is safe:
public static double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) total += n.doubleValue(); // safe: all are Numbers
return total;
}
sum(List.of(1, 2, 3)); // List<Integer>: accepted
sum(List.of(1.5, 2.5)); // List<Double>: accepted
sum(new ArrayList<Number>()); // List<Number>: accepted
Writing is still forbidden, because the exact subtype is unknown:
public static void broken(List<? extends Number> numbers) {
Number first = numbers.get(0); // OK: reading as Number is guaranteed
// numbers.add(1); // ERROR: the list might be List<Double>
// numbers.add(1.5); // ERROR: it might be List<Integer>
}
A lower bounded wildcard, ? super T, is the mirror image: "some supertype
of T". You know the list accepts a T, because a T fits into any container
of its supertypes, so writing is safe:
public static void addNumbers(List<? super Integer> target) {
target.add(1); // OK: an Integer fits a List<Integer>,
target.add(2); // a List<Number>, or a List<Object>
// Integer first = target.get(0); // ERROR: might be List<Object>
Object any = target.get(0); // only Object is guaranteed on read
}
addNumbers(new ArrayList<Integer>()); // accepted
addNumbers(new ArrayList<Number>()); // accepted
addNumbers(new ArrayList<Object>()); // accepted
// addNumbers(new ArrayList<Double>()); // ERROR: Double is not a supertype of Integer
Object <- ? super Integer accepts these three
|
Number <-
|
Integer <- the pivot: T itself is in both ranges
|
(Integer's subtypes) <- ? extends Number accepts Integer, Double, ...
? extends Number : read as Number, cannot write
? super Integer : write Integer, read only as Object
The three forms compared:
| Wildcard | Accepts | Safe to read as | Safe to write |
|---|---|---|---|
List<Number> | Exactly List<Number> | Number | Number and subtypes |
List<? extends Number> | Number and subtypes | Number | Nothing but null |
List<? super Integer> | Integer and supertypes | Object | Integer and subtypes |
List<?> | Any list | Object | Nothing but null |
Note the asymmetry that catches people: ? extends gives you a useful read
type and no write; ? super gives you a useful write type and no useful
read. You never get both, and that limitation is exactly what makes them safe.
A wildcard bound may name an interface, and unlike a type parameter bound it
cannot list multiple bounds with &:
List<? extends Comparable<?>> items; // interface bound: fine
// List<? extends Number & Comparable<?>> x; // ERROR: not allowed on wildcards
Key Takeaways
? extends TacceptsTand its subtypes, allows reading asT, and forbids writing.? super TacceptsTand its supertypes, allows writing aT, and allows reading only asObject.- The restrictions follow from what the compiler can guarantee about the unknown type.
- You never get both safe reading and safe writing from one wildcard.
- Wildcards take a single bound;
&is only for type parameter bounds.
🧪 Practice
- Write
double total(List<? extends Number>)and call it with three different numeric list types.- Write
void fill(List<? super String> target)and call it withList<String>,List<CharSequence>, andList<Object>.- Interview: Why can you not add to a
List<? extends Number>even though every element is aNumber? (Hint: name two concrete lists the parameter might refer to.)
The PECS Principle
Knowing what each wildcard permits is one thing; knowing which to reach for when designing a method is another. The mnemonic that settles it is PECS: Producer Extends, Consumer Super.
Ask what the parameter does from the method's point of view:
- If it produces values the method reads, use
? extends T. - If it consumes values the method writes, use
? super T. - If it does both, use a plain
Twith no wildcard.
The canonical example is a copy method, which has one of each:
public static <T> void copy(List<? extends T> source, // PRODUCER: we read from it
List<? super T> target) { // CONSUMER: we write to it
for (T item : source) { // reading as T is safe: extends
target.add(item); // writing a T is safe: super
}
}
That signature is far more flexible than the naive one. It accepts any pairing where the source elements fit the target:
List<Integer> integers = List.of(1, 2, 3);
List<Number> numbers = new ArrayList<>();
List<Object> objects = new ArrayList<>();
copy(integers, numbers); // Integer -> Number: fine
copy(integers, objects); // Integer -> Object: fine
copy(numbers, objects); // Number -> Object: fine
// copy(numbers, integers); // ERROR, correctly: a Number is not an Integer
// Compare with the rigid version:
// static <T> void copy(List<T> source, List<T> target)
// copy(integers, numbers); // would FAIL: T cannot be both Integer and Number
The standard library follows PECS consistently, and recognising it makes the Javadoc readable:
// Collection: addAll consumes nothing from the argument except reads, so extends
boolean addAll(Collection<? extends E> c);
// Comparator is a consumer of the elements it compares, so super
void sort(Comparator<? super E> comparator);
// Stream: map produces R and consumes T
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// Collections.copy, the textbook case
public static <T> void copy(List<? super T> dest, List<? extends T> src);
SOURCE TARGET
List<? extends T> List<? super T>
"I produce T values" "I consume T values"
| ^
+---------- copy ---------------+
reads T, writes T
Two refinements complete the picture. A return type should not use a wildcard, because it forces every caller to deal with the wildcard too:
// Poor: callers now hold a List<? extends Number> and cannot add to it.
public static List<? extends Number> parse(String input) { ... }
// Better: commit to a type.
public static List<Number> parse(String input) { ... }
And a parameter the method only inspects, never reading elements out by type
nor adding to, may simply use List<?>:
public static int size(Collection<?> any) { return any.size(); }
| Parameter role | Use | Example |
|---|---|---|
| Method reads elements from it | ? extends T | addAll(Collection<? extends E>) |
| Method writes elements into it | ? super T | sort(Comparator<? super E>) |
| Method does both | Plain T | List<T> swap(List<T>) |
| Method ignores the element type | ? | size(Collection<?>) |
| Return type | No wildcard | List<Number> parse(...) |
Key Takeaways
- PECS: use
extendsfor parameters you read from andsuperfor those you write to.- A parameter doing both gets a plain type parameter with no wildcard.
- PECS-typed parameters accept far more caller types than rigid ones.
- The standard library applies PECS throughout, which is why its signatures look the way they do.
- Do not put wildcards in return types; they propagate the awkwardness to callers.
🧪 Practice
- Write
<T> void copy(List<? extends T> src, List<? super T> dst)and exercise it with three type pairings.- Explain, for
Collections.max(Collection<? extends T>, Comparator<? super T>), why each wildcard is the one it is.- Interview: Why does
List.sorttakeComparator<? super E>rather thanComparator<E>? (Hint: could a comparator written forObjectsort aList<String>?)
Type Erasure and Its Consequences
Generics arrived in Java 5, fifteen years into the language's life, with millions of lines of existing code that had to keep working and keep interoperating. The solution was erasure: generics are a compile-time feature, and the compiler removes them once it has finished checking.
After erasure, every type parameter is replaced by its leftmost bound, or
by Object if it has none, and the compiler inserts the casts that the
programmer no longer writes.
// What you write:
public class Box<T> {
private T contents;
public T get() { return contents; }
public void put(T item) { contents = item; }
}
// What the bytecode effectively contains:
public class Box {
private Object contents; // T erased to Object
public Object get() { return contents; }
public void put(Object item) { contents = item; }
}
// With a bound, erasure uses the bound instead of Object:
public class NumberBox<T extends Number> { private T value; }
// becomes: private Number value;
At the call site the compiler inserts the cast it proved was safe:
Box<String> box = new Box<>();
String s = box.get();
// compiles to: String s = (String) box.get(); <- a checked cast, inserted
The immediate consequence is that generic type information does not exist at runtime. Two parameterisations of the same class are the same class:
List<String> strings = new ArrayList<>();
List<Integer> ints = new ArrayList<>();
System.out.println(strings.getClass() == ints.getClass()); // true
System.out.println(strings.getClass().getName()); // java.util.ArrayList
Everything that follows from that is worth listing, because each one looks arbitrary until you know the cause:
| You cannot | Because at runtime |
|---|---|
new T() | T is unknown; there is no class to instantiate |
new T[10] | Same, and arrays need a real component type |
x instanceof List<String> | Only List exists; the argument is gone |
Overload on List<String> vs List<Integer> | Both erase to the same signature |
| Catch a generic exception type | The catch mechanism needs a real class |
| Use a primitive as a type argument | Object cannot hold an int directly |
// public static void f(List<String> l) { }
// public static void f(List<Integer> l) { }
// ERROR: both methods have the same erasure f(List)
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) { } // ERROR: illegal generic type
if (list instanceof List<?>) { } // legal: the only permitted form
Erasure also explains the unchecked warning. When the compiler cannot verify a cast because the type is erased, it says so and shifts responsibility to you:
@SuppressWarnings("unchecked") // only after proving it is safe
public static <T> T cast(Object value) {
return (T) value; // unchecked: nothing verifies T here
}
String s = cast("hello"); // fine
// Integer i = cast("hello"); // compiles! fails at the USE site
That last line shows the real hazard: an unchecked cast defers the
ClassCastException to wherever the value is finally used, which may be far
away. Suppress the warning only when you can prove safety by inspection, and
comment why.
Erasure is not all loss. It gave Java migration compatibility: pre-generics code and generic code call each other freely, since they compile to the same bytecode. C# chose reified generics and a cleaner runtime model, at the cost of a breaking change to its runtime. Java chose not to break the world.
Key Takeaways
- Erasure removes type parameters after compile-time checking, replacing them with their leftmost bound or
Object.- The compiler inserts the casts you no longer write.
List<String>andList<Integer>are the same class at runtime.- Every generic restriction in the language traces back to erasure.
- Erasure bought interoperability with pre-generics code, which was the design goal.
🧪 Practice
getClass()forList<String>andList<Integer>and explain the result.- Write two overloads differing only in type argument and read the erasure error.
- Interview: Why did Java implement generics with erasure instead of reifying them? (Hint: what already existed in 2004 that could not be recompiled?)
Restrictions on Generic Arrays and Statics
Two restrictions cause more confusion than the rest combined, and both follow directly from erasure.
You cannot create a generic array. new T[10] is rejected, and so is
new List<String>[10]:
public class Stack<T> {
// private T[] items = new T[10]; // ERROR: generic array creation
}
// List<String>[] lists = new List<String>[10]; // ERROR: generic array creation
The reason is that arrays are reified: an array knows its component type at runtime and checks every store against it. A generic array would have nothing to check, and the resulting hole is easy to demonstrate:
// Suppose generic array creation were allowed:
// List<String>[] arrays = new List<String>[1]; // pretend this compiles
// Object[] objects = arrays; // arrays are covariant
// objects[0] = List.of(42); // stores a List<Integer>!
// // the store check sees only List
// String s = arrays[0].get(0); // ClassCastException, silently
The store check would see List on both sides and allow it, defeating the
guarantee generics exist to provide. Two workarounds are standard:
public class Stack<T> {
private Object[] items = new Object[10]; // 1: store as Object[]
private int size;
@SuppressWarnings("unchecked")
public T pop() {
return (T) items[--size]; // cast on the way out
}
}
public class Stack2<T> {
private final T[] items;
@SuppressWarnings("unchecked")
public Stack2(int capacity) {
items = (T[]) new Object[capacity]; // 2: create Object[], cast once
}
}
// 3: avoid arrays entirely, which is usually best
public class Stack3<T> {
private final List<T> items = new ArrayList<>();
}
A reflective alternative exists when the caller can supply the class:
public static <T> T[] newArray(Class<T> type, int size) {
@SuppressWarnings("unchecked")
T[] array = (T[]) java.lang.reflect.Array.newInstance(type, size);
return array; // the type token supplies what
} // erasure removed
You cannot use a class's type parameter in a static context. Static members belong to the class, not to any instance, and the type argument is supplied per instance:
public class Box<T> {
// private static T shared; // ERROR: non-static T in static context
// public static T create() { return null; } // ERROR: same reason
public static <U> Box<U> empty() { // a static GENERIC method is fine
return new Box<>(); // U is declared by the method
}
}
The logic: Box<String> and Box<Integer> share one static field after
erasure, so a static T would have to be String and Integer at once. A
static method may declare its own parameter, because that one is supplied per
call.
A few further restrictions round out the set:
// public class BadException<T> extends Exception { }
// ERROR: a generic class may not extend Throwable (catch needs a real type)
// try { } catch (T e) { } // ERROR: cannot catch a type parameter
class Box<T> {
// T t = new T(); // ERROR: cannot instantiate a type parameter
}
The standard fix for the last one is a factory or a Supplier:
public static <T> List<T> fill(int count, Supplier<T> factory) {
List<T> result = new ArrayList<>();
for (int i = 0; i < count; i++) result.add(factory.get()); // caller supplies
return result;
}
List<StringBuilder> builders = fill(3, StringBuilder::new);
| Restriction | Standard workaround |
|---|---|
new T() | Pass a Supplier<T> or Class<T> |
new T[n] | Object[] plus a cast, or a List |
Static field of type T | Make it a static generic method |
Generic subclass of Throwable | Use a non-generic exception |
instanceof List<String> | instanceof List<?> |
Key Takeaways
- Generic array creation is banned because arrays check stores at runtime and generics are erased.
- Use
Object[]with a contained cast, or prefer aList, and suppress the warning only when provably safe.- A class's type parameter cannot appear in a static field or static method.
- Static methods may declare their own type parameters, which are supplied per call.
- A type parameter cannot be instantiated, caught, or used to extend
Throwable.
🧪 Practice
- Implement a generic
Stack<T>backed byObject[]with a single suppressed cast.- Try declaring
private static T cache;in a generic class and explain the error.- Write a
<T> List<T> fill(int, Supplier<T>)and use it to create threeArrayListinstances.
Recursive Generic Bounds
Some contracts refer back to the type implementing them. Comparable is the
clearest case: a type is not comparable in general, it is comparable to
itself. Expressing that requires a bound that mentions the parameter being
declared, which looks circular and is not.
public static <T extends Comparable<T>> T max(List<T> items) {
// ^ ^
// the parameter ...used in its own bound
T best = items.get(0);
for (T item : items) if (item.compareTo(best) > 0) best = item;
return best;
}
Read it as a constraint, not a definition: "T is any type that can be
compared to T". The compiler simply checks that whatever you substitute
satisfies the condition.
max(List.of("b", "a", "c")); // String implements Comparable<String>: fine
max(List.of(3, 1, 2)); // Integer implements Comparable<Integer>: fine
The pattern has a name, the curiously recurring generic pattern, and its main use is making a method return the caller's own type rather than a supertype. Without it, fluent builders break under inheritance:
// Without recursion: the superclass methods return the SUPERCLASS type.
public class Builder {
public Builder name(String n) { ...; return this; }
}
public class AdvancedBuilder extends Builder {
public AdvancedBuilder extra(int x) { ...; return this; }
}
new AdvancedBuilder()
.extra(1)
.name("x") // returns Builder...
// .extra(2) // ERROR: Builder has no extra()
;
The recursive bound fixes the chain by threading the concrete type through:
public abstract class Builder<SELF extends Builder<SELF>> {
protected String name;
@SuppressWarnings("unchecked")
protected SELF self() { return (SELF) this; } // the one unchecked cast
public SELF name(String name) {
this.name = name;
return self(); // returns the SUBCLASS type
}
}
public class AdvancedBuilder extends Builder<AdvancedBuilder> {
// ^^^^^^^^^^^^^^^ passes itself
private int extra;
public AdvancedBuilder extra(int extra) {
this.extra = extra;
return this;
}
}
new AdvancedBuilder()
.extra(1)
.name("x") // now returns AdvancedBuilder
.extra(2); // so the chain continues
Builder<SELF extends Builder<SELF>>
|
| SELF = AdvancedBuilder
v
AdvancedBuilder extends Builder<AdvancedBuilder>
name() declared to return SELF -> returns AdvancedBuilder
The same shape appears in java.lang.Enum, which is declared
Enum<E extends Enum<E>> precisely so that compareTo accepts only constants
of the same enum type:
public abstract class Enum<E extends Enum<E>> implements Comparable<E> { ... }
// Consequence: you cannot compare constants of different enums.
// Day.MON.compareTo(Status.PAID); // ERROR, and correctly so
Two honest cautions. The pattern does not guarantee correctness: a subclass
can pass the wrong type argument, as in
class Wrong extends Builder<SomethingElse>, and the compiler accepts it. And
the self() cast is unchecked by necessity. Use the pattern where the fluent
chain genuinely matters, and prefer simpler designs elsewhere, since the
declarations are hard to read.
Key Takeaways
- A recursive bound like
<T extends Comparable<T>>constrains a type to relate to itself.- It is a constraint the compiler checks, not a circular definition.
- The self-referential form
<SELF extends Builder<SELF>>lets inherited methods return the subclass type.java.lang.Enumuses it so comparisons stay within one enum type.- The pattern needs one unchecked
self()cast and does not prevent a subclass supplying the wrong argument.
🧪 Practice
- Write
<T extends Comparable<T>> T min(List<T>)and call it on strings and integers.- Build a two-level fluent builder with
<SELF extends Builder<SELF>>and chain methods from both levels in any order.- Interview: Why is
Enumdeclared asEnum<E extends Enum<E>>? (Hint: what should happen if you compare a constant of one enum with a constant of another?)
<a id="7-the-collections-framework"></a>