skip to content

If generics are erased, how is it that reflection can still recover declared type arguments at runtime? What role does the class-file Signature attribute play?

level: seniorimportance: must knowfreq 62%

answer

  1. Erasure hits runtime type, not declaration metadata
  2. Signature attribute holds full generic descriptor
  3. JVM ignores it; reflection parses it
  4. getGenericType / getGenericReturnType / getGenericSuperclass
  5. Declarations recoverable, values-in-hand not

basics

~20 s

Erasure only changes the object's runtime type. The compiler still records the full generic form of declarations (superclass, fields, method params/returns) in a hidden class-file 'Signature' attribute. Reflection reads that attribute to report the original type arguments.

solid answer

~50 s

Erasure removes generic type arguments from the *runtime type* of objects, but the compiler does not discard generic information that appears in *declarations*. For a class's superclass and interfaces, for field types, and for method/constructor parameter and return and exception types, javac emits a `Signature` attribute into the class file holding the full generic descriptor (e.g. `Ljava/util/List<Ljava/lang/String;>;`). The JVM ignores this attribute for execution — linkage uses the erased descriptors — but the reflection API parses it on demand. So `Field.getGenericType()`, `Method.getGenericParameterTypes()`, `Method.getGenericReturnType()`, and `Class.getGenericSuperclass()` return `Type` objects such as `ParameterizedType` carrying the declared arguments, whereas the non-generic counterparts (`getType()`, `getReturnType()`) return only the erased `Class`. The crucial limit: only generics *in a declaration's signature* survive. The type argument of a value held in a *local variable* or inside an object instance is not recorded anywhere and cannot be recovered.

code

java · 23 lines
java
import java.lang.reflect.*;
import java.util.*;

class Repo {
    Map<Long, List<String>> cache;
    List<Integer> find(Set<String> keys) { return null; }
}

public class SignatureDemo {
    public static void main(String[] a) throws Exception {
        Field f = Repo.class.getDeclaredField("cache");
        System.out.println(f.getType());          // interface java.util.Map  (erased)
        System.out.println(f.getGenericType());   // java.util.Map<java.lang.Long, java.util.List<java.lang.String>>

        Method m = Repo.class.getDeclaredMethod("find", Set.class);
        System.out.println(m.getGenericReturnType());        // java.util.List<java.lang.Integer>
        System.out.println(m.getGenericParameterTypes()[0]); // java.util.Set<java.lang.String>

        // But a value in hand reveals nothing:
        List<String> live = new ArrayList<>();
        System.out.println(live.getClass());      // class java.util.ArrayList  -- no <String>
    }
}

go deeper

for a junior

Knows that reflection has 'generic' variants of methods and that they can sometimes show <String>, without needing the class-file detail.

for a middle

Can use getGenericType()/getGenericReturnType(), cast to ParameterizedType, and call getActualTypeArguments() to read a field or method's element type.

for a senior

Explains the Signature attribute, the erased-descriptor-vs-generic-signature duality, exactly which declarations carry it, and the firm declaration-vs-value-in-hand boundary.

for a principal

Reasons about why the JVM retains-but-ignores the attribute (separate compilation, tooling), implications for obfuscation/bytecode rewriting, and frames the whole reflection-on-generics capability as deliberate metadata design.

## The apparent paradox If `List<String>` is erased to `List`, how do frameworks like Spring, Jackson, Gson, Hibernate and Guice know the element type of a field or method parameter at runtime? The answer is that **erasure applies to the runtime *type of objects*, not to the text of *declarations* recorded in the class file.** Java keeps two parallel descriptions of every declaration: 1. The **erased descriptor** — what the JVM actually uses for method resolution, field access and verification. For a method `List<String> m(Map<Integer,String> x)` the descriptor is `(Ljava/util/Map;)Ljava/util/List;` — no type arguments. 2. The **generic signature** — an *optional* `Signature` attribute that javac attaches alongside, holding the full generic form, e.g. `(Ljava/util/Map<Ljava/lang/Integer;Ljava/lang/String;>;)Ljava/util/List<Ljava/lang/String;>;`. ## What a class file is, briefly A Java `.class` file is a structured binary: a constant pool plus tables describing the class, its fields and its methods, and a list of **attributes** on each. Attributes are named, optional metadata blobs; the JVM skips attributes it does not recognise. `Signature` is one such attribute, defined in the JVM Specification. It exists on the class itself (for the generic superclass/interfaces and the class's own type parameters), on each generic field, and on each generic method/constructor. ## Where Signature attributes are (and are not) emitted The compiler emits a `Signature` attribute **only where a generic type appears in a declaration's signature**: - a class's `extends`/`implements` clause and its own `<T>` declaration → `Class.getGenericSuperclass()`, `getGenericInterfaces()`, `getTypeParameters()`; - a **field**'s declared type → `Field.getGenericType()`; - a **method/constructor**'s parameter, return, and `throws` types → `Method.getGenericParameterTypes()`, `getGenericReturnType()`, `getGenericExceptionTypes()`. It is **not** emitted for the contents of a method body. A local variable `List<String> tmp` leaves no recoverable generic trace, and an *instance* of `ArrayList<String>` carries nothing on the object itself. ## How reflection reads it The `java.lang.reflect` API has paired methods: | Erased (Class) | Generic (Type) | |---|---| | `Field.getType()` | `Field.getGenericType()` | | `Method.getReturnType()` | `Method.getGenericReturnType()` | | `Method.getParameterTypes()` | `Method.getGenericParameterTypes()` | | `Class.getSuperclass()` | `Class.getGenericSuperclass()` | The generic variants parse the `Signature` attribute and return `java.lang.reflect.Type` instances. `Type` is a marker interface with subtypes: - **`Class<?>`** — a plain non-generic type; - **`ParameterizedType`** — e.g. `List<String>`; ask `getRawType()` (→ `List`) and `getActualTypeArguments()` (→ `[String]`); - **`TypeVariable`** — a type parameter like `T`; - **`WildcardType`** — `? extends Number`; - **`GenericArrayType`** — `T[]` or `List<String>[]`. ### Example ```java class Box { List<String> items; } Field f = Box.class.getDeclaredField("items"); f.getType(); // interface java.util.List (erased) ParameterizedType pt = (ParameterizedType) f.getGenericType(); pt.getRawType(); // interface java.util.List pt.getActualTypeArguments()[0]; // class java.lang.String <-- recovered! ``` The `String` came from the `Signature` attribute on the `items` field, **not** from any runtime object. ## Why the JVM keeps but ignores it For *execution* the JVM uses only the erased descriptors — that is what keeps generics zero-cost and backward-compatible. The `Signature` attribute is purely informational: compilers use it for separate compilation (so a downstream module sees the generic API of a class it didn't compile with), and reflection/tools/frameworks use it for runtime introspection. If you strip it (some obfuscators do), the code still runs but `getGenericType()` degrades to the erased form. ## The hard boundary The single most important thing to internalise: **you can recover the type argument of a declaration, never of a value in hand.** Given `void process(List<String> l)` you can find `String` via `getGenericParameterTypes()`. Given just `List<String> l` as a local, or an `ArrayList<String>` object, the argument is unrecoverable. This is exactly why frameworks ask you to provide the declaration (a field, a method, or a *super-type token*) rather than the object itself.

  • Name the reflection methods you'd use to recover declared type arguments, and the Type subtype you'd cast to.
    Field.getGenericType(), Method.getGenericReturnType()/getGenericParameterTypes(), Class.getGenericSuperclass()/getGenericInterfaces()/getTypeParameters(). For a parameterized type cast to ParameterizedType and call getActualTypeArguments(); other subtypes are TypeVariable, WildcardType, GenericArrayType, and plain Class.
  • What happens to getGenericType() if a tool strips the Signature attribute from a class file?
    It silently falls back to the erased type — getGenericType() then returns the same plain Class as getType(). The code still runs because the JVM never needed the attribute; only introspection loses fidelity.

saying these in an interview costs you the question

  • Saying reflection reads the type argument off the live object
  • Claiming ALL generic info is erased so reflection can never see it
  • Confusing getType() (erased Class) with getGenericType() (Type)
  • Believing a local variable's generic type is recoverable
  • Thinking the JVM uses the Signature attribute for method dispatch

context