Explain the 'super type token' (TypeReference) idiom: how does subclassing a generic type let you capture a full generic type at runtime despite erasure?
answer
- Anonymous subclass: new TypeReference<...>(){}
- Type goes into the superclass declaration
- getGenericSuperclass -> ParameterizedType -> getActualTypeArguments
- Jackson/Gson/Guice/Spring all do this
- The {} braces are mandatory
basics
~20 sYou make an anonymous subclass of a generic base, like new TypeReference<List<String>>(){}. Because a subclass's superclass is a declaration, its generic argument is stored in the Signature attribute. The base reads it via getClass().getGenericSuperclass() and recovers List<String>.
solid answer
~40 sA super type token works around the fact that you cannot capture a generic type from a value or a class literal. You create an *anonymous subclass* of a generic helper, e.g. `new TypeReference<List<String>>(){}`. The trick: although the *value's* type argument would be erased, the subclass's `extends TypeReference<List<String>>` clause is a *declaration*, so the compiler records `List<String>` in the subclass's class-file Signature attribute. Inside the base class you call `getClass().getGenericSuperclass()`, cast to `ParameterizedType`, and call `getActualTypeArguments()[0]` to recover the full `List<String>` Type — raw type plus its argument. Jackson's `TypeReference`, Gson's `TypeToken`, Guice's `TypeLiteral`, and Spring's `ParameterizedTypeReference` all use this. It only works because you supply the type through inheritance (a declaration the Signature attribute captures), not through a runtime object — which is exactly the declaration-vs-value-in-hand boundary of erasure.
code
java · 23 linesimport java.lang.reflect.*;
import java.util.*;
abstract class TypeRef<T> {
final Type type;
protected TypeRef() {
ParameterizedType pt = (ParameterizedType) getClass().getGenericSuperclass();
this.type = pt.getActualTypeArguments()[0];
}
}
public class TokenDemo {
public static void main(String[] args) {
// Anonymous subclass: the {} is mandatory.
TypeRef<Map<Long, List<String>>> ref = new TypeRef<Map<Long, List<String>>>() {};
System.out.println(ref.type);
// -> java.util.Map<java.lang.Long, java.util.List<java.lang.String>>
ParameterizedType pt = (ParameterizedType) ref.type;
System.out.println(pt.getRawType()); // interface java.util.Map
System.out.println(Arrays.toString(pt.getActualTypeArguments())); // [class java.lang.Long, java.util.List<java.lang.String>]
}
}go deeper
Recognises new TypeReference<List<String>>(){} as the way frameworks like Jackson deserialize a generic collection.
Can write and use a TypeReference/TypeToken, knows the {} matters, and that it gives back a Type usable by a parser.
Explains the full mechanism: type baked into the superclass declaration, recovered via getGenericSuperclass()/getActualTypeArguments(), and ties it to the Signature attribute and declaration-vs-value boundary.
Discusses limits (method-level type variables not reified, allocation/caching, Signature-stripping fragility), surveys the framework implementations, and could design such an API correctly.
## The problem it solves Suppose you write a JSON parser and want `parse(json, ???)` to deserialize into a `List<Person>`. You cannot pass `List<Person>.class` — that literal does not exist (erasure). And passing a `List<Person>` *object* tells you nothing at runtime, because the object is erased to `ArrayList`. You need a way to *hand the compiler a generic type and have it preserved to runtime*. The **super type token** (a.k.a. *Gafter's gadget*, after Neal Gafter) does exactly that. ## The mechanism step by step Recall the key fact from the Signature-attribute topic: **the generic type in a *declaration* survives in the class-file `Signature` attribute, and `Class.getGenericSuperclass()` can read it.** A class's `extends Foo<Bar>` clause is such a declaration. 1. Define an abstract generic holder: ```java abstract class TypeRef<T> { private final Type type; protected TypeRef() { Type superclass = getClass().getGenericSuperclass(); // TypeRef<List<String>> this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; } public Type getType() { return type; } } ``` 2. The caller instantiates an **anonymous subclass**, baking the desired type into the `extends` clause: ```java TypeRef<List<String>> ref = new TypeRef<List<String>>() {}; ``` The `{}` is essential — it creates a *new subclass* whose superclass is `TypeRef<List<String>>`. That superclass declaration is recorded in the anonymous class's `Signature` attribute. 3. At runtime, `getClass()` on that instance returns the anonymous subclass; `getGenericSuperclass()` returns the `ParameterizedType` `TypeRef<List<String>>`; `getActualTypeArguments()[0]` is the `Type` for `List<String>` — a `ParameterizedType` whose raw type is `List` and whose argument is `String`. The **whole** generic type, nested arguments and all, is recovered. ## Why the empty braces matter Without `{}` you would have `new TypeRef<List<String>>()` — but `TypeRef` is abstract here, and more importantly *no subclass is created*, so there is no `extends TypeRef<List<String>>` declaration to read. The token works *because of inheritance*: the type argument lives in a subclass's superclass declaration, which is exactly the kind of declaration the Signature attribute preserves. This is the canonical demonstration that **the declaration-vs-value distinction is the whole game** — you smuggle the type into a declaration rather than a value. ## Real-world implementations - **Jackson**: `new TypeReference<List<Person>>(){}` → `mapper.readValue(json, ref)`. - **Gson**: `new TypeToken<List<Person>>(){}.getType()`. - **Guice**: `TypeLiteral<List<Person>>` for binding generic types. - **Spring**: `ParameterizedTypeReference<List<Person>>` for `RestTemplate`/`WebClient` responses. All are the same idiom with different names. ## Limits and pitfalls - It captures **fully reified, concrete** type arguments only. `new TypeRef<List<T>>(){}` where `T` is itself a type variable of the *enclosing* generic method captures a `TypeVariable`, not a concrete type — the concrete `T` is still erased. You must spell out the concrete type at the construction site. - It allocates a small object (the anonymous subclass instance) per token; usually negligible, sometimes cached. - If a bytecode tool strips Signature attributes, the token degrades — `getGenericSuperclass()` falls back to the raw `Class` and the cast to `ParameterizedType` fails. ## The takeaway A super type token is the practical, idiomatic answer to 'how do I pass `List<String>` as a value at runtime?': you cannot pass the *type argument of a value*, so you encode it as the *type argument of a superclass declaration*, where erasure's Signature-attribute retention lets reflection read it back.
- Why are the empty braces {} after new TypeReference<...>() required?They create an anonymous subclass, so there is an 'extends TypeReference<List<String>>' declaration. That superclass declaration is what gets recorded in the Signature attribute and read back via getGenericSuperclass(). Without {} no subclass exists and there is nothing to read.
- Does new TypeRef<List<T>>(){} inside a generic method capture the concrete T?No. T is itself erased in that context, so getActualTypeArguments() yields a TypeVariable, not the concrete type. The token only captures concrete types written literally at the construction site.
saying these in an interview costs you the question
- Omitting the {} and expecting it to work
- Believing it captures the type of a runtime object
- Thinking you can capture a method-level type variable T concretely
- Confusing it with passing List<String>.class (which does not exist)
- Claiming it bypasses erasure rather than exploiting retained signatures