skip to content

At runtime, what does List<?> look like, and how does it relate to type erasure and operations like instanceof?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Erasure: type args gone in bytecode
  2. All List<X> share one runtime class List
  3. Only instanceof List<?> (or raw) is legal
  4. List<?> is reifiable; List<String> is not
  5. new List<?>[] legal; new List<String>[] illegal

basics

~10 s

At runtime Java erases generic type information, so List<String> and List<Integer> are both just List. Because of that, the only generic instanceof check Java allows is the unbounded wildcard form: obj instanceof List<?>.

solid answer

~40 s

Java generics use type erasure: type arguments exist only at compile time and are removed in bytecode, so all parameterizations of List share the single runtime class java.util.List. List<String>.class is not a thing - there is only List.class. This is why you cannot write obj instanceof List<String> (the runtime has no way to check the element type); the only legal generic instanceof and the only legal cast target of that shape is the unbounded wildcard: obj instanceof List<?>, because <?> adds no runtime information to check. Similarly you can write a generic array type only as the unbounded-wildcard reifiable type, e.g. List<?>[]. The unbounded wildcard is special precisely because it is the parameterized form that survives erasure unchanged - it asserts nothing about the element type, which is exactly what the runtime can verify.

go deeper

for a junior

Knows generics are erased at runtime and that you can write instanceof List<?> but not instanceof List<String>.

for a middle

Explains erasure means all List parameterizations share one runtime class and that the wildcard is the only legal generic instanceof form.

for a senior

Connects reifiability, erasure, instanceof, casts, and array creation, explaining why List<?> is the runtime-honest form.

for a principal

Reasons about erasure tradeoffs (compatibility vs reification) and guides patterns (e.g. type tokens) for code that needs runtime type information generics cannot provide.

## Type erasure: the runtime forgets type arguments Java implemented generics with **type erasure**: generic type information is used by the compiler for checking, then **erased** from the compiled bytecode. After compilation: ``` List<String> -> List List<Integer> -> List List<?> -> List ``` All of them are the **same** runtime class, `java.util.List`. There is no `List<String>.class`; there is only `List.class`. Two variables `List<String>` and `List<Integer>` will even report `a.getClass() == b.getClass()` as `true`. ## A type is 'reifiable' if its full type is available at runtime A **reifiable** type is one whose type information is fully present at runtime. `String`, `Integer`, `List` (raw), and `List<?>` are reifiable. `List<String>` is **not** reifiable, because the `String` part was erased. ## Why `instanceof List<String>` is illegal The `instanceof` operator is a runtime check. To evaluate `obj instanceof List<String>`, the JVM would need to inspect the element type at runtime - but erasure deleted it. So the language **forbids** `instanceof` (and casts) against non-reifiable parameterized types: ``` if (obj instanceof List<String>) { } // COMPILE ERROR if (obj instanceof List<?>) { } // OK if (obj instanceof List) { } // OK (raw, but prefer the wildcard) ``` The **only** generic form allowed with `instanceof` is the **unbounded wildcard** `List<?>` (and the raw type, which you should avoid). It works because `<?>` asserts nothing about the element type - there is nothing for the runtime to check beyond 'is it a List?', which is exactly what is reifiable. ## Generic arrays and the wildcard Array creation has the same constraint. You cannot create `new List<String>[10]` (generic array creation is illegal, again because of erasure + array covariance unsoundness). But the unbounded-wildcard array type `List<?>[]` **is** reifiable, so `new List<?>[10]` is legal. This is another place the unbounded wildcard is the special, runtime-safe form. ## Casting Casting to `(List<String>)` compiles but only with an **unchecked** warning, because the cast cannot actually be verified at runtime - the `<String>` part is unenforceable. Casting to `(List<?>)` is fully checkable. So the wildcard is again the honest, runtime-truthful form. ## Why this matters The unbounded wildcard is not just an API-design convenience; it is the **parameterized type that matches what the runtime actually knows**. Whenever you need a generic type in a runtime-sensitive position - `instanceof`, a cast you want checked, an array type - the unbounded wildcard is the form the language permits, precisely because erasure leaves it with no extra claims to verify.

  • What is a reifiable type and is List<?> one?
    A reifiable type has its complete type information available at runtime. List<?> is reifiable (the wildcard adds nothing to check), but List<String> is not, because the element type is erased.
  • Why can you create new List<?>[10] but not new List<String>[10]?
    Generic array creation requires a reifiable component type. List<?> is reifiable so the array is allowed; List<String> is not reifiable, so its array creation is forbidden to keep the type system sound.

saying these in an interview costs you the question

  • Thinking obj instanceof List<String> is allowed - it is a compile error
  • Believing List<String>.class exists - only List.class does
  • Assuming a cast to List<String> is runtime-checked - it is unchecked
  • Confusing 'reifiable' so that List<String> is treated as fully runtime-typed

context