skip to content

How does Java's 'no instanceof with parameterized types' restriction compare to C#, where `obj is List<string>` works — and why did Java make this design choice?

level: principalimportance: nice to knowfreq 22%

answer

  1. C# reifies, Java erases
  2. Java: migration compatibility with pre-generics code
  3. Erasure → one runtime class per generic
  4. Cost: no instanceof T / new T[] / heap pollution
  5. Project Valhalla may add partial reification

basics

~20 s

C# reifies generics: the runtime keeps the type argument, so obj is List<string> works. Java erases generics, so the type argument is gone at runtime and the check is impossible. Java chose erasure to stay backward-compatible with pre-generics code and bytecode.

solid answer

~40 s

C# implements generics by **reification**: the CLR creates and tracks a distinct runtime type for each closed generic like `List<string>`, so `obj is List<string>` and even `typeof(T)` work at runtime. Java implements generics by **erasure**: the type argument is checked at compile time then discarded, so all `List<…>` share one runtime class and the type test is unanswerable — hence the restriction. Java chose erasure deliberately for **migration compatibility**: generics were retrofitted onto Java 5 without changing the JVM or breaking the billions of lines of pre-generics code and libraries; a raw `List` and `List<String>` had to interoperate seamlessly. The cost is the well-known erasure limitations (no `instanceof T`, no `new T[]`, heap pollution). Project Valhalla is exploring partial reification, but classic Java generics remain erased.

go deeper

for a junior

Aware that other languages (C#) can test generic types at runtime and Java cannot.

for a middle

Names erasure vs reification as the core difference and that Java's choice was for compatibility.

for a senior

Explains migration compatibility, lists the concrete trade-offs (instanceof, new T, boxing, heap pollution), and the C# reification consequences.

for a principal

Treats it as a language-design case study: weighs ecosystem continuity vs runtime power, anticipates Valhalla's direction, and applies the reasoning when designing cross-language or long-lived APIs.

## Two philosophies of generics **Reification** (C#/.NET): the runtime *keeps* generic type arguments. The CLR JIT-instantiates a separate concrete type for each closed generic — `List<int>`, `List<string>` are genuinely different runtime types. Consequences: `obj is List<string>` works, `typeof(T)` works, `new T()` works (with a `new()` constraint), and value types avoid boxing because `List<int>` stores raw ints. **Erasure** (Java): generic type arguments are a *compile-time-only* check. After type-checking, the compiler **erases** them — `List<String>` → `List`, `T` → `Object`/bound — producing bytecode indistinguishable from pre-generics code. Consequences: a single runtime class per generic, no `instanceof List<String>`, no `instanceof T`, no `new T[]`, and possible *heap pollution* (an unchecked cast lets a `String` list secretly hold an `Integer`). ## Why Java picked erasure: migration compatibility When generics were added in **Java 5 (2004)**, there were already enormous amounts of compiled code and libraries using raw `List`, `Map`, etc. Sun's hard requirement was **migration compatibility**: existing pre-generics binaries had to keep running unchanged, and new generic code had to interoperate with old raw-typed code in *both* directions (you can pass a `List<String>` where a raw `List` is expected and vice-versa, with only a warning). The cleanest way to guarantee that — without forking the JVM or duplicating every collection class — was to make `List<String>` and raw `List` the *same* runtime type. Erasure achieves exactly that. The JVM didn't need any new instructions, and old `.class` files still linked. C# took the opposite path because .NET generics shipped in **C# 2 / .NET 2.0 (2005)** as a *runtime* feature designed in; Microsoft was willing to extend the CLR itself, so reification was feasible without the same legacy burden. ## The trade-off ledger | | Java (erasure) | C# (reification) | |---|---|---| | `obj instanceof/is List<String>` | ❌ illegal | ✅ works | | `new T()` / `typeof(T)` | ❌ (use Class<T> token) | ✅ (with constraints) | | Value types in collections | boxed (until Valhalla) | no boxing | | Backward-compat with raw types | ✅ seamless | n/a (no legacy raw types) | | Runtime/metadata cost | minimal (one class) | per-instantiation code/metadata | | Code bloat risk | none | possible (many instantiations) | Erasure also gives Java a subtle benefit: no per-instantiation code bloat and trivial interop with reflective/raw code — at the price of the developer ergonomics around `instanceof`, arrays, and type tokens. ## The future: Project Valhalla Java's **Project Valhalla** (value types / specialized generics) aims to allow generics over primitives without boxing and may bring *some* reified type information, narrowing the gap. But as of standard Java, generics are erased, and the `instanceof` restriction is a permanent consequence of the 2004 backward-compatibility decision — a textbook case of a language trading runtime power for ecosystem continuity. ## Why this matters to a principal This comparison is the canonical example of **how a backward-compatibility constraint shapes a language for decades**. Understanding it lets you reason about *why* the workarounds (type tokens, super-type tokens, `Class<T>` plumbing) are idiomatic in Java but absent in C#, and to set expectations when porting code or designing cross-language APIs.

  • What concrete capabilities does reification give C# that erasure denies Java?
    Runtime type tests on closed generics (`obj is List<string>`), `typeof(T)` / `default(T)`, `new T()` with a constructor constraint, and unboxed value-type collections (`List<int>` stores raw ints). Java emulates the first three with `Class<T>` tokens and pays boxing for the last (pre-Valhalla).
  • What is 'migration compatibility' and why did it force erasure?
    It's the requirement that pre-generics binaries keep running and that generic and raw code interoperate both ways. Making List<String> and raw List the same runtime type satisfied it without changing the JVM or duplicating collection classes — which erasure does by construction.

saying these in an interview costs you the question

  • Claiming Java erases for performance reasons (it was backward compatibility)
  • Saying C# also erases generics
  • Asserting Valhalla has already shipped full reified generics in standard Java
  • Confusing erasure with type inference (var) — unrelated

context