skip to content

What is a super type token (the TypeReference idiom), and how does subclassing let it capture a full parameterized type like List<String> despite erasure?

level: seniorimportance: should knowfreq 38%

answer

  1. Anonymous subclass + concrete arg → Signature attribute keeps it
  2. getGenericSuperclass() → ParameterizedType → getActualTypeArguments()[0]
  3. Supertype generic signatures are NOT erased (only instances are)
  4. Trailing {} is mandatory — no subclass, nothing to read
  5. Jackson TypeReference, Gson/Guava TypeToken, Spring ParameterizedTypeReference
  6. Captures only statically-known types, not a type variable T

basics

~20 s

A super type token captures a full generic type like List<String> by creating an anonymous subclass that fixes the type argument. Java writes that argument into the class's Signature metadata, and the token reads it back at runtime with getGenericSuperclass — something a plain Class can't do.

solid answer

~40 s

A plain `Class<T>` token can't represent `List<String>` because there's only one `List.class`. The super type token idiom (Jackson's `TypeReference`, Guava's `TypeToken`, Gson's) works around this. You write `new TypeReference<List<String>>(){}`—note the trailing `{}` that creates an **anonymous subclass**. When you subclass a generic type and supply a concrete argument, the compiler is required to record the actual type argument in the class file's **Signature attribute** (generic signatures of supertypes are *not* erased, even though instances are). At runtime the base class calls `getClass().getGenericSuperclass()`, casts it to `ParameterizedType`, and reads `getActualTypeArguments()[0]`, recovering the full `List<String>` as a `java.lang.reflect.Type`. So the type that erasure removes from *instances* survives in the *subclass's supertype declaration*, and subclassing is what makes it readable. The captured `Type` then drives reflective deserialization that needs the element type.

code

java · 16 lines
java
import java.lang.reflect.*;

public abstract class TypeReference<T> {
    private final Type type;
    protected TypeReference() {
        Type sup = getClass().getGenericSuperclass(); // TypeReference<List<String>>
        if (sup instanceof Class)                       // someone forgot the {}
            throw new IllegalStateException("missing type parameter");
        this.type = ((ParameterizedType) sup).getActualTypeArguments()[0];
    }
    public Type getType() { return type; }
}

// usage — note the trailing {} that creates an anonymous subclass
TypeReference<java.util.List<String>> ref = new TypeReference<>(){};
System.out.println(ref.getType()); // java.util.List<java.lang.String>

go deeper

for a junior

Recognizes new TypeReference<List<String>>(){} from Jackson/Gson usage and knows it lets a library know the element type.

for a middle

Knows the trailing {} makes an anonymous subclass and that this is how the parameterized type is captured for deserialization.

for a senior

Explains that supertype generic signatures are retained in the Signature attribute, walks getGenericSuperclass → ParameterizedType → getActualTypeArguments, and knows it fails for a type variable T.

for a principal

Reasons about the class-file Signature mechanism vs erasure, the per-token class-loading cost, the java.lang.reflect.Type model edge cases, and when to prefer this over reified-generics alternatives or codegen.

## Why a plain token isn't enough A `Class<T>` token captures a **simple** type (`User.class`). It cannot capture `List<String>`, because `List<String>.class` doesn't exist—there is exactly one `List.class` shared by every parameterization. Erasure means an *instance* of `List<String>` carries no record of its `String` element type. Yet libraries like Jackson must know the element type to deserialize `["a","b"]` into a `List<String>` rather than a `List<Object>`. The **super type token** recovers that information. ## The crucial exception: generic *signatures* of supertypes are not erased Erasure deletes generic type arguments from running objects, but the **class file format keeps a separate optional `Signature` attribute** that records the *generic* form of a class's superclass, interfaces, fields, and method signatures. This exists so the compiler can type-check code compiled against your class later. Reflection exposes it via the `java.lang.reflect.Type` hierarchy (`getGenericSuperclass()`, `getGenericInterfaces()`, generic field/return types). **Key insight:** the type argument you bake into a `extends`/`implements` clause is preserved in metadata, even though the same argument on a local variable or field *value* is erased. ## The idiom, step by step ```java public abstract class TypeReference<T> { private final Type type; protected TypeReference() { Type superClass = getClass().getGenericSuperclass(); // superClass is TypeReference<List<String>> — a ParameterizedType this.type = ((ParameterizedType) superClass).getActualTypeArguments()[0]; } public Type getType() { return type; } } TypeReference<List<String>> ref = new TypeReference<List<String>>(){}; // ^^ trailing braces ref.getType(); // => List<String> as a ParameterizedType ``` 1. **`new TypeReference<List<String>>(){}`** — the trailing `{}` makes an **anonymous subclass** of `TypeReference`. Without it you'd have a bare instance of `TypeReference` whose superclass is `Object`, and there'd be no parameterized supertype to read. 2. Because the anonymous class `extends TypeReference<List<String>>` with a *concrete* argument, the compiler writes `List<String>` into that subclass's **`Signature`** for its superclass. 3. In the constructor, `getClass()` returns the anonymous subclass; `getGenericSuperclass()` returns a `ParameterizedType` representing `TypeReference<List<String>>` (read from the Signature, not erased). 4. `getActualTypeArguments()[0]` extracts the `List<String>` `Type`. That `Type` is a full `ParameterizedType` (raw type `List`, one type argument `String`), enough to drive nested reflective work. ## Why subclassing specifically Reflection can read the generic *declaration* of a supertype but not the generic *type argument of an instance*. By forcing the type into an `extends` clause via an anonymous subclass, you convert ephemeral, erased instance-level type info into durable, readable class-level metadata. No subclass ⇒ no parameterized supertype ⇒ nothing to recover. This is the whole trick. ## Real-world tokens - **Jackson** `com.fasterxml.jackson.core.type.TypeReference<T>` → `mapper.readValue(json, new TypeReference<List<User>>(){})`. - **Gson** `com.google.gson.reflect.TypeToken<T>` → `gson.fromJson(json, new TypeToken<List<User>>(){}.getType())`. - **Guava** `com.google.common.reflect.TypeToken<T>` (full type-resolution toolkit). - **Spring** `ParameterizedTypeReference<T>` for `RestTemplate`/`exchange`. ## Costs and caveats - Each token is a tiny extra class loaded by the JVM (negligible, but real). - It only captures types written **literally** at the call site. `new TypeReference<List<T>>(){}` where `T` is a type variable does **not** work—`T` is itself erased in *this* method's frame, so the Signature records the variable `T`, not a concrete type. Tokens must be statically known. - The recovered value is a `java.lang.reflect.Type`, not a `Class`; downstream code must handle `ParameterizedType`, `WildcardType`, `GenericArrayType`, etc. ## Mental model Erasure shreds the type tag off the *object*, but if you staple the type onto a *subclass's family tree* (`extends Foo<List<String>>`), Java keeps that tag in the class file's Signature—and reflection lets you peel it back off.

  • Why is the trailing `{}` in `new TypeReference<List<String>>(){}` essential?
    It creates an anonymous subclass whose superclass is the parameterized TypeReference<List<String>>. Reflection can read that parameterized supertype from the class file's Signature. Without {}, you'd have a plain TypeReference whose superclass is Object, so there's nothing to recover.
  • Does `new TypeReference<List<T>>(){}` work inside a generic method where T is a type variable?
    No. T is itself erased in that method's frame, so the recorded Signature is the type variable T, not a concrete type. Super type tokens only capture types known literally at compile time.
  • If instance generics are erased, how can the supertype's type argument survive?
    Because the class file keeps a separate Signature attribute recording the generic forms of supertypes/fields/methods (so later compilation can type-check against the class). Erasure affects runtime instances, not this metadata; reflection reads it via getGenericSuperclass().

saying these in an interview costs you the question

  • Saying super type tokens prove Java generics are reified — they aren't; this exploits retained class metadata, not runtime instance types.
  • Forgetting the trailing {} and expecting the type to be recoverable.
  • Claiming it works with a type variable T at the call site.
  • Confusing the returned java.lang.reflect.Type with Class — it's a ParameterizedType and needs different handling.
  • Thinking getClass() on the instance gives Object's superclass — getGenericSuperclass on the anonymous subclass gives the parameterized type.

context