Generics fundamentals

Type parameters, type erasure, why you cannot new T(), raw types, and the unchecked warning you should not suppress.

9 min read🗂️ Collections and Generics

Generics let you write List<Order> and have the compiler stop you putting a Customer in it. That is the whole purpose: type errors found at compile time instead of ClassCastException at run time. The design choice that makes generics confusing — erasure — was made so that Java 5 code could run on Java 4 JVMs, and its consequences are still with us. This lesson is those consequences, exactly what the compiler emits in place of the types it erased, the two tricks that recover a type at run time, and the ClassCastException that appears three files away from the line that caused it.

Type parameters

java
public class Box<T> {
    private final T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
}
 
Box<String> b = new Box<>("hi");     // the diamond infers <String>
String s = b.get();                  // no cast

T is a type parameter; String is the type argument. Conventions: T for a type, E for an element, K/V for key and value, R for a return type. A method can have its own:

java
public static <T> List<T> repeat(T item, int n) {
    List<T> out = new ArrayList<>(n);
    for (int i = 0; i < n; i++) out.add(item);
    return out;
}
List<String> xs = repeat("a", 3);    // T inferred as String

The <T> before the return type declares the method's parameter. The caller almost never writes it; inference fills it in.

Erasure

At run time, Box<String> and Box<Integer> are the same class: Box. The compiler checks the type arguments, inserts casts where values come out, and then erases the parameters — T becomes Object (or its bound). The .class file for Box contains no String.

Under the hood: what the compiler emits instead

Run javap -c -p on the two snippets above and erasure stops being a slogan:

Box after erasureplaintext
private final java.lang.Object value;            // T became Object
public java.lang.Object get();                   // descriptor: ()Ljava/lang/Object;
  Signature: ()TT;                               // the generic type, kept for reflection only
String s = b.get(), at the call siteplaintext
invokevirtual Box.get:()Ljava/lang/Object;
checkcast     java/lang/String                   // the compiler inserted this; it is where a ClassCastException fires
astore_2

Three things follow. The cast is at the read. Whatever went into the box, the exception fires where a caller reads it as a String, not where the wrong thing was stored. Generic information survives in Signature attributes on classes, fields and methods, which is how Jackson knows a field is List<Order>, how Spring resolves Repository<User>, and how getGenericReturnType() works. What does not survive is the type argument of any particular object: a List<String> instance is an ArrayList and nothing more. Overriding across erasure needs bridges. class IntBox extends Box<Integer> { @Override public Integer get() {...} } has an Integer get() that does not override the erased Object get(), so javac adds a synthetic Object get() that calls it, which is the bridge method from the Inheritance lesson.

Consequences you will hit:

java
new T()                        // no: T does not exist at run time
new T[10]                      // no: same reason
T.class                        // no
instanceof List<String>        // no: only List is checkable
static T field                 // no: static members are per class, and there is one class
List<String>.class             // no such thing; List.class only
void f(List<String> a) {}      // and
void f(List<Integer> a) {}     // no: same erasure, "name clash"
class E<T> extends Exception   // no: catch blocks match erased classes

Workarounds exist for each — pass a Class<T> token, use Array.newInstance, use instanceof List<?> — but the model to keep is: generic types are a compile-time discipline over a run-time that only knows raw types.

Recovering a type at run time

Two tricks cover most real needs. A class token: <T> T parse(String json, Class<T> type) carries the class as a value, which is why mapper.readValue(json, Order.class) works and why it cannot express List<Order> (there is no List<Order>.class). A super type token: an anonymous subclass of a generic class keeps its supertype's arguments in its Signature attribute, so new TypeReference<List<Order>>() {} is a real class whose getGenericSuperclass() reports TypeReference<List<Order>>. Jackson's TypeReference, Spring's ParameterizedTypeReference and Guava's TypeToken are all this one trick, and it is the reason those calls have the odd {} at the end.

Raw types and the unchecked warning

java
List raw = new ArrayList();            // raw type: generics switched off
raw.add(42);
List<String> strings = raw;            // compiles, with a warning
String s = strings.get(0);             // ClassCastException here, far from the cause

A raw type is a pre-generics escape hatch. Using one produces an "unchecked" warning, which is the compiler saying "I can no longer guarantee this". The state it leaves behind has a name, heap pollution: a variable of type List<String> referring to a list that holds an Integer. The ClassCastException appears where the value is read, not where the wrong type went in. Treat unchecked warnings as errors in your own code; @SuppressWarnings("unchecked") is acceptable only on the narrowest scope with a comment proving why it is safe (typically: a generic array creation you fully control, as ArrayList does with its Object[] elementData).

Walkthrough: the ClassCastException three files away

A service deserialises a response with List<User> users = mapper.readValue(json, List.class);. It compiles with an unchecked warning nobody reads. It passes the test that checks users.size().

  1. Jackson is asked for a List with no element type, so it builds an ArrayList of LinkedHashMap<String, Object>, one per JSON object. That is the only thing it can build for an unknown element type.
  2. The assignment to List<User> is unchecked; no cast is emitted, because the erased types match. The list is now heap-polluted: declared List<User>, holding maps.
  3. users.size() works. users.isEmpty() works. Logging users works and prints maps, which nobody noticed.
  4. Three files away, String name = users.get(0).getName(); compiles to invokeinterface List.get; checkcast User; invokevirtual User.getName. The checkcast throws ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.acme.User, with a stack trace pointing at a line that did nothing wrong.
  5. The fix is the super type token: mapper.readValue(json, new TypeReference<List<User>>() {}), which gives Jackson the element type through the Signature attribute, and the warning that would have prevented it is the one the build should have failed on.

Erasure means the type system's guarantee is only as good as the last unchecked operation. Find that operation and the exception's true location is there, however far the trace points.

Invariance

List<Integer> is not a List<Number>, even though Integer is a Number:

java
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints;              // does not compile
nums.add(3.14);                        // if it did, ints would contain a Double

Generic types are invariant in their arguments, and this example is why: allowing the assignment would let you write the wrong type through the wider reference. Arrays, by contrast, are covariant — Integer[] is an Object[] — and the same mistake compiles and throws ArrayStoreException at run time, because an array knows its element type and a generic collection does not. Generics chose the compile-time error. The next lesson shows how wildcards let you relax invariance safely when you only read or only write.

Bounded type parameters

java
public static <T extends Comparable<T>> T max(List<T> xs) {
    T best = xs.get(0);
    for (T x : xs) if (x.compareTo(best) > 0) best = x;
    return best;
}

T extends Comparable<T> says T must be comparable to itself; inside the method, compareTo is available. Multiple bounds use &: <T extends Number & Comparable<T>>. The erasure of a bounded T is its first bound, which is why max compiles to a method taking a List and returning a Comparable, and why a call through the second bound compiles to a checkcast to it first.

The fully general signature you will see in the JDK is <T extends Comparable<? super T>>, which also accepts a type whose superclass implements Comparable — wildcards again, next lesson.

Generic classes in practice

Most application code uses generics — List<Order>, Map<String, Money>, Optional<User>, Function<A, B> — and writes a generic type rarely: a Result<T, E>, a Page<T>, a Repository<T, ID>. When you do write one, keep the parameter list short and let inference do the work at call sites. Project Valhalla's specialised generics, still in progress, are the long-term answer to the boxing cost of List<Integer>; until then, primitive streams and arrays are.

Try it yourself

Where does it throw?

java
static <T> T first(List<T> xs) { return xs.get(0); }
List raw = new ArrayList<>(List.of(42));
List<String> strings = raw;                 // warning
Object o = first(strings);                  // line A
String s = first(strings);                  // line B
Answer

Line A runs fine: first returns Object after erasure, and assigning to Object needs no cast. Line B throws ClassCastException: the compiler inserted checkcast String at the assignment, and the value is an Integer. The pollution happened at the List<String> strings = raw line; the exception fires at the first read that needs the type. Same list, same method, one line apart.

Why does the token work?

mapper.readValue(json, Order.class) works; mapper.readValue(json, List<Order>.class) does not compile; mapper.readValue(json, new TypeReference<List<Order>>() {}) works. Explain all three in terms of what exists at run time.

Answer

Order.class is a real Class object; the class exists. List<Order> has no class of its own, only List, so there is no literal. The anonymous subclass new TypeReference<List<Order>>() {} is a real class, and its Signature attribute records that it extends TypeReference<List<Order>>; getGenericSuperclass() reads that back as a ParameterizedType with Order as the argument. The type survives because it was written in a class declaration, which the compiler preserves, not in an expression, which it erases.

Name clash

java
class Stats {
    double avg(List<Integer> xs) { ... }
    double avg(List<Double> xs) { ... }
}

Why does this not compile, and what are two ways to write it?

Answer

Both methods erase to avg(List), the same descriptor, and a class cannot have two methods with the same erased signature: name clash: avg(List<Double>) and avg(List<Integer>) have the same erasure. Either give them different names (avgInts, avgDoubles), or write one method over List<? extends Number> that calls doubleValue() on each element, which is what the next lesson's wildcards are for.

Misconceptions

  • "Generics are checked at run time." They are erased. The run time sees List and Object, plus checkcast instructions the compiler left at every read.
  • "A ClassCastException points at the bug." It points at the read. The bug is the last unchecked operation that let the wrong object in, and the unchecked warning marked it.
  • "Reflection cannot see generic types." It can see declared ones (Signature attributes on classes, fields, methods, and anonymous subclasses). It cannot see an instance's type argument, because instances do not have one.
  • "@SuppressWarnings("unchecked") makes the cast safe." It makes the warning disappear. The cast is safe only if you can prove nothing else can reach the array or collection.
  • "Arrays and generics have the same variance." Arrays are covariant and checked at run time (ArrayStoreException); generics are invariant and checked at compile time. That is why generic arrays cannot be created.

Going deeper

  • JLS §4.6 (type erasure), §4.8 (raw types), §4.12.2 (heap pollution) and §8.4.8.3 (bridge methods).
  • javap -v on any generic class, for the Signature attributes and the checkcast at each call site.
  • Angelika Langer's Java Generics FAQ, still the most complete reference on every corner case.
  • Effective Java, items 26 (no raw types), 27 (eliminate unchecked warnings), 28 (prefer lists to arrays), 33 (typesafe heterogeneous containers).
  • Jackson's TypeReference and Spring's ParameterizedTypeReference source: the super type token, twenty lines each.
Progress is saved on this device and to your account when signed in.