skip to content

Given Java's erasure model, what type information actually survives at runtime, and how do type tokens and super type tokens exploit exactly that to recover what's lost?

level: principalimportance: nice to knowfreq 25%

answer

  1. Survives: raw class of object, Signature metadata of declarations, array component type
  2. Lost: a live object's own type args, type variables in-frame
  3. Class token = raw class as a value; super type token = retained supertype Signature
  4. Signature attribute exists for separate compilation; reflection reads it as java.lang.reflect.Type
  5. new T[] banned: arrays reified vs T erased — model clash
  6. Recovered Type informs deserialization; element cast still unchecked

basics

~20 s

At runtime Java keeps the raw class of each object but throws away its generic type arguments. Two things survive: an object's own class (so Class tokens work) and the generic Signature metadata of declared classes/fields/methods (so super type tokens can read a baked-in parameterized supertype). Both idioms recover only what's still there.

solid answer

~50 s

Java implements generics by erasure, so the design question is: what survives, and what do the token idioms actually lean on? Three things persist at runtime. First, every object's **raw class** is reified (getClass()/instanceof), which is why a `Class<T>` token works for simple types and `Class.cast`/`isInstance` give real checks. Second, **generic signatures of declarations**—supertype, field, method-parameter, and return types—are kept in the class file's `Signature` attribute (for separate compilation), readable via `java.lang.reflect.Type`. Super type tokens exploit exactly this by baking the parameterization into an anonymous subclass's `extends` clause. Third, **array component types** are reified (so you can't make `new T[]`). What is *not* kept is the parameterization of a live object: a `List<String>` instance is indistinguishable from `List<Integer>`. Tokens are therefore not magic—they recover only erasable-but-recorded info: pass the type as a value (Class), or smuggle it through retained declaration metadata (super type token). Anything requiring a live object's own argument is genuinely unrecoverable.

go deeper

for a junior

Knows generics are erased and that you pass a Class to keep the type around; not expected to detail what metadata survives.

for a middle

Distinguishes the reified raw class (Class token) from erased instance parameterization and knows super type tokens capture full types.

for a senior

Explains the Signature attribute, what survives vs is lost, and maps each token idiom to the survivor it exploits, including the new T[] clash.

for a principal

Frames erasure's survivor/loss split as a soundness and API-design boundary, weighs reification/codegen/Valhalla trade-offs, and coaches the team on token limits (static-only, metadata-not-instance, residual unchecked element casts).

## The framing Java chose **erasure** for generics (for migration compatibility with pre-generics bytecode). The consequence is a precise, sometimes surprising split between type info that **survives to runtime** and info that is **gone**. Mastering type tokens means knowing that split exactly, because the idioms recover *only* what survives. ## What survives at runtime 1. **An object's raw (erased) class is reified.** `obj.getClass()` and `obj instanceof Foo` always work; every object knows its own class. This is the foundation of the **`Class<T>` type token**: you pass `Foo.class` as a value, and `token.isInstance(x)` / `token.cast(x)` are real, sound runtime checks. It only captures the *simple* class—`List.class`, never `List<String>.class`. 2. **Generic *signatures of declarations* are retained in the class file.** The JVM class format has an optional **`Signature` attribute** attached to classes, fields, and methods. It records the *generic* form of: the superclass and superinterfaces, field types, and method parameter/return/throws types. Its purpose is **separate compilation**: a compiler reading your already-compiled class must see `List<String>` to type-check callers. Reflection surfaces it through the `java.lang.reflect.Type` hierarchy—`getGenericSuperclass()`, `getGenericInterfaces()`, `Field.getGenericType()`, `Method.getGenericReturnType()`, etc. **Super type tokens exploit precisely this:** an anonymous subclass `extends TypeReference<List<String>>{}` writes `List<String>` into the subclass's superclass Signature, which is then read back. The parameterization survives because it lives on a *declaration*, not on a *live instance*. 3. **Array component types are reified.** `String[]` knows it holds Strings; storing the wrong type throws `ArrayStoreException`. This is why generic array creation `new T[n]` is forbidden—`T` is erased but arrays demand a reified component type, so the two models clash. 4. **Enum constants, annotations (with retention), and the bytecode-level raw types** also persist, but are tangential here. ## What does NOT survive - **A live object's own type arguments.** A `List<String>` *instance* carries nothing distinguishing it from `List<Integer>`. There is no reflective call that returns the element type of an arbitrary list you're handed. This is the hard floor: no token can recover it from the object alone—you must have captured the type *before* erasure, statically. - **Type variables in the current frame.** Inside `<T> void m()`, `T` is erased; `new TypeReference<List<T>>(){}` records the variable `T`, not a concrete type. ## How each idiom maps to the survivors | Idiom | Survivor it exploits | Captures | |---|---|---| | `Class<T>` type token | reified raw class (passed as a value) | a simple type (`User`) | | `Class.cast`/`isInstance` | reified raw class of the object | runtime check vs that simple type | | Super type token (`TypeReference`) | retained Signature on a subclass's supertype | a full parameterized type (`List<User>`) | | Field/return inspection | retained Signature on fields/methods | parameterization of a declared member | ## Design consequences (the principal view) - **Soundness boundary.** `token.cast` is sound for the simple type; super type tokens hand you a `Type` you must interpret, and any element-level cast you then perform is conventionally still unchecked—the recovered `Type` *informs* deserialization but doesn't make the eventual write type-checked by the JVM. - **API design.** 'Carry the type as a value' (Class / TypeReference parameters) is the idiomatic Java answer to the absence of reification—used pervasively (Jackson, Gson, Guava, Spring `ParameterizedTypeReference`, JPA `find`, DI containers). It trades a little caller ceremony for sound type recovery. - **Alternatives and trade-offs.** Reified generics (C#) avoid all this but break migration compatibility and bloat the runtime; Java deliberately didn't. Project Valhalla and pattern matching shift some ergonomics but not the erasure core. Codegen (annotation processors) is the heavyweight alternative when you need fully static, reflection-free type handling. - **Limits to teach the team.** Tokens must be statically known; they don't help with `T` in scope; the recovered info is metadata, not a live-object property; per-anonymous-token there's a class-load cost. ## Mental model Erasure burns the object's generic clothes but spares two things: every object still knows its own *raw skeleton* (Class token), and the *family tree you declare* keeps its generic labels in metadata (super type token). The idioms are just two ways of reading those two survivors.

  • Why is `new T[n]` forbidden while `(T) obj` is merely warned?
    Arrays are reified — an array stores and enforces its component type at runtime (ArrayStoreException). T is erased, so the JVM has no reified component type to create the array with, making it impossible rather than just unverifiable. A cast is only unverifiable, hence a warning, not an error.
  • Can you reflectively obtain the element type of an arbitrary `List` instance handed to you at runtime?
    No. A live List instance's element type is erased and not stored. You can only recover parameterization that was recorded in a declaration's Signature (a field, method, or a super type token captured before erasure), never from the bare object.
  • Why does the Signature attribute exist at all if generics are erased?
    For separate compilation: a compiler reading your already-compiled class needs the generic forms of its supertypes, fields, and methods to type-check code that uses them. Erasure governs runtime instances; Signature governs compile-time-visible declarations.

saying these in an interview costs you the question

  • Saying 'all generic info is erased' without qualification — declaration Signatures and raw classes survive.
  • Claiming you can get the element type from any List instance at runtime.
  • Treating super type tokens as evidence of reification rather than retained metadata.
  • Believing the recovered Type makes downstream element casts JVM-checked.
  • Confusing array reification with generic erasure when explaining new T[].

context