Given type erasure removes generic types at runtime, how can reflection still recover that a field is declared as List<String>?
answer
- erasure hits instances, not declarations
- Signature attribute stores generic decls
- getGenericType / getGenericReturnType
- cast to ParameterizedType
- getRawType + getActualTypeArguments
- super type token trick
basics
~20 sErasure removes generics from the values (objects) at runtime, but the generic types written in declarations (fields, method signatures, superclasses) are kept in the class file. Reflection reads them via getGenericType, and you inspect the result as a ParameterizedType to get String.
solid answer
~40 sType erasure means a List<String> object at runtime is just a List — the element type isn't carried by the instance. But the *declaration site* metadata is retained. Methods like Field.getGenericType, Method.getGenericReturnType/getGenericParameterTypes, and Class.getGenericSuperclass return a java.lang.reflect.Type that preserves the generic signature. For List<String> that Type is a ParameterizedType: you call getRawType() to get List and getActualTypeArguments() to get [String.class]. The distinction is declaration metadata (retained, reflectable) versus instance type information (erased, gone). This is exactly how Jackson/Gson resolve element types, and why patterns like 'super type tokens' (an anonymous subclass capturing the type argument in getGenericSuperclass) work. You cannot, however, recover the type argument of an arbitrary List object you were handed — only from a typed declaration.
code
java · 15 linesimport java.lang.reflect.*;
import java.util.*;
class Holder { List<String> names; Map<String,Integer> counts; }
public class Demo {
public static void main(String[] args) throws Exception {
Field f = Holder.class.getDeclaredField("names");
System.out.println(f.getType()); // interface java.util.List (erased)
Type g = f.getGenericType(); // java.util.List<java.lang.String>
ParameterizedType pt = (ParameterizedType) g;
System.out.println(pt.getRawType()); // interface java.util.List
System.out.println(pt.getActualTypeArguments()[0]); // class java.lang.String
}
}go deeper
Knows generics are 'erased' at runtime and that you generally can't get the element type of a list object.
Distinguishes erased instance info from retained declaration metadata and can use getGenericType + ParameterizedType.getActualTypeArguments to extract String from a List<String> field.
Maps the full Type hierarchy (ParameterizedType/TypeVariable/WildcardType/GenericArrayType), recurses into nested generics, and explains the super type token pattern used by serialization libs.
Weighs erasure's design trade-offs (compatibility vs. reified generics), guides framework/library API design around Type/ParameterizedTypeReference, and anticipates edge cases like wildcards, bounds, and generic arrays in a type-resolution engine.
## Generics and type erasure (the starting point) **Generics** let you write `List<String>` to say 'a list whose elements are Strings', giving compile-time type checking. **Type erasure** is the implementation choice Java made: after the compiler checks your generics, it *erases* the type parameters so the bytecode mostly works with the raw type (`List`). This kept Java backward-compatible with pre-generics code. A practical consequence: at run time a `List<String>` **object** carries no record that its elements are Strings — `list.getClass()` is just `java.util.ArrayList`. `new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass()` is `true`. ## The key insight: declarations are not erased Erasure removes type-argument information from **values/objects**, but the **generic signatures of declarations** are written into the `.class` file in a special metadata attribute (the *Signature* attribute). 'Declarations' here means: field types, method return types and parameter types, the extends/implements clause, and type-variable bounds. The JVM keeps this so reflection can reconstruct the source-level generic types. ## The reflection API for reading it For every non-generic accessor there is a parallel *generic* one that returns a `java.lang.reflect.Type` instead of a `Class`: - `Field.getType()` → `Class` (erased: `List`); `Field.getGenericType()` → `Type` (`List<String>`). - `Method.getReturnType()` vs `Method.getGenericReturnType()`. - `Method.getParameterTypes()` vs `Method.getGenericParameterTypes()`. - `Class.getSuperclass()` vs `Class.getGenericSuperclass()`. `Type` is a marker super-interface with several sub-interfaces: - `Class<?>` — a plain class (`String`). - `ParameterizedType` — a generic invocation like `List<String>` or `Map<String,Integer>`. - `TypeVariable<?>` — a type parameter such as the `T` in `class Box<T>`. - `WildcardType` — `? extends Number`. - `GenericArrayType` — `T[]` or `List<String>[]`. ## Inspecting a ParameterizedType When `getGenericType()` returns a `ParameterizedType`, you cast and ask: - `getRawType()` → the erased class, `List.class`. - `getActualTypeArguments()` → an array of the type arguments; for `List<String>` that is `[String.class]`; for `Map<String,Integer>` it is `[String.class, Integer.class]`. - `getOwnerType()` → the enclosing type for nested generics. Each actual type argument is itself a `Type`, so you can recurse (e.g. `List<Map<String,Long>>`). ## What you still cannot do Reflection recovers generics from **declarations**, never from arbitrary **instances**. If a method receives `List<?> list`, there is no way at run time to discover whether the caller passed a `List<String>` or `List<Integer>` — that information was erased from the object. You can only read the type argument where it was *written down* in some declaration the JVM retained (a field, a method signature, or a captured supertype). ## Why it matters in practice This mechanism powers serialization/deserialization libraries (Jackson's `TypeReference`, Gson's `TypeToken`, Spring's `ParameterizedTypeReference`). They use the **super type token** trick: create an anonymous subclass `new TypeReference<List<String>>(){}` so the generic argument is baked into the class's `getGenericSuperclass()`, which reflection can then read — because a (super)class declaration *is* retained. ```java Field f = Holder.class.getDeclaredField("names"); // List<String> names; Type t = f.getGenericType(); ParameterizedType pt = (ParameterizedType) t; Type arg = pt.getActualTypeArguments()[0]; // String.class ```
- How does Jackson/Gson capture a full generic type like List<String> for deserialization?Via a 'super type token': an anonymous subclass (e.g. new TypeReference<List<String>>(){}) bakes the type argument into its generic superclass, which Class.getGenericSuperclass() exposes as a ParameterizedType to reflection.
- What does getActualTypeArguments() return for a raw List with no type argument?You wouldn't get a ParameterizedType at all — getGenericType() returns the plain Class List.class for a raw declaration, so there are no actual type arguments to read.
saying these in an interview costs you the question
- Claiming you can read the type argument of any List object at runtime
- Saying erasure deletes ALL generic info from the class file
- Using getType() instead of getGenericType() and expecting type arguments
- Forgetting to handle the case where getGenericType() returns a plain Class (raw type)