skip to content

Erased vs reified generics: what are the trade-offs of Java's erasure design versus reified generics like C#, and how do projects like Valhalla relate?

level: principalimportance: should knowfreq 30%

answer

  1. Erased = compile-time only; reified = runtime type arg kept
  2. Java erasure driven by migration/backward compatibility
  3. Reified enables new T[], typeof(T), no-boxing value types
  4. Erasure cost: boxing of List<Integer>, no runtime type ops
  5. Valhalla = value types + primitive specialization, not full reification

basics

~20 s

Erased generics (Java) drop type arguments at runtime, giving backward compatibility and one class per generic type but losing runtime type info. Reified generics (C#) keep the arguments at runtime, enabling new T[] and typeof(T), at the cost of more runtime machinery and a clean break from old code.

solid answer

~50 s

Java chose **erasure**: type arguments are compile-time only, so generic and pre-generics code interoperate (migration compatibility), there is one class file per generic type regardless of instantiation, and the JVM needed no new bytecode. The price is no runtime type info — no `new T[]`, `T.class`, `instanceof List<String>`, no per-argument specialization, and reflection only recovers types from *declarations* via the Signature attribute. **C#/.NET reified** generics keep the argument at runtime: `typeof(List<int>)` is distinct from `typeof(List<string>)`, `new T[]` and runtime type tests work, and value-type instantiations like `List<int>` are *specialized* to avoid boxing — at the cost of a CLR-level mechanism, more metadata, and the fact that .NET could break from a pre-generics world Java couldn't. Java's **Project Valhalla** does not reify generics wholesale; it adds value/primitive classes and *generic specialization over primitives* so future generics can avoid boxing, narrowing erasure's performance gap while preserving compatibility.

go deeper

for a junior

Knows Java generics are erased (gone at runtime) and that some other languages keep them, without the trade-off detail.

for a middle

Contrasts erased vs reified at a high level: Java can't do new T[]/T.class; C# can; Java keeps backward compatibility.

for a senior

Articulates the concrete trade-offs (compatibility, code size, boxing, runtime type ops) and the workarounds erasure forces; aware Valhalla targets boxing.

for a principal

Frames erasure as a constraint-driven 2004 decision, separates expressiveness vs performance costs, accurately positions Valhalla (specialization, not full reification), and avoids absolutist judgments.

## Two design philosophies **Generics** let a type be parameterized by another type (`List<T>`). The deep design question is: *does the type argument exist at runtime?* - **Erased (Java):** No. The argument is a compile-time fiction used for checking, then stripped (see the erasure topic). Runtime sees only the raw type. - **Reified (C#/.NET):** Yes. `List<int>` and `List<string>` are genuinely different runtime types the CLR knows about. ## Why Java picked erasure: migration compatibility Generics arrived in Java 5 (2004), ~9 years into a huge ecosystem of compiled, pre-generics code. The overriding constraint was that **old code and new generic code must interoperate seamlessly**: - A generified `ArrayList` must still be usable by code written against the raw `ArrayList`, and vice versa (this is why **raw types** exist and why `List` and `List<String>` are assignment-compatible with only warnings). - A single, unchanged-on-the-surface class file serves every instantiation — no class-size explosion, no new bytecode, no JVM changes. Erasure delivered all of this. .NET, designed with generics from a clean slate (generics shipped in .NET 2.0, 2005, but the platform was young), did not carry that constraint and could afford a runtime mechanism. ## What each design buys and costs | Aspect | Java (erased) | C# (reified) | |---|---|---| | Runtime type arg | Gone (recoverable only from declarations) | Present on the type | | `new T[]`, `T.class`/`typeof(T)`, `instanceof T` | Forbidden | Allowed | | One class per generic type | Yes (compact) | Specialized per value-type arg | | Value types (`List<int>`) | Boxed to `List<Integer>` (heap, indirection) | Specialized, no boxing | | Backward/migration compat | Excellent | N/A (clean start) | | Runtime machinery / metadata | Minimal | Heavier (JIT specialization, type metadata) | | Reflection on generics | Via Signature attribute (declarations only) | Full runtime generic reflection | ## The boxing cost is the sharpest edge Under erasure, `List<Integer>` actually stores boxed `Integer` objects — each `int` becomes a heap object with a header, hurting memory density and cache behavior. C#'s `List<int>` stores raw `int`s contiguously. For numeric/data-heavy workloads this is Java's most painful erasure cost, and the main thing Valhalla targets. ## Project Valhalla — the nuanced answer A common misconception is that Valhalla 'reifies Java generics'. It largely does **not**. Its pillars: 1. **Value classes / primitive classes** — identity-free types the JVM can flatten and stack-allocate (no header, no indirection). 2. **Generic specialization over primitives** — letting future generics be instantiated over `int` directly, so `List<int>` need not box. This narrows erasure's *performance* gap. Valhalla deliberately keeps the *compatibility* benefits of erasure (no per-reference-type class explosion, existing code keeps working) while attacking the boxing cost. It is not a switch to C#-style full reification of reference-type arguments. ## How to discuss this as a principal - Frame it as a *constraint-driven* decision: erasure was the right call **given Java's compatibility mandate in 2004**, not a mistake. - Distinguish the two costs of erasure: (a) **expressiveness** (no runtime type ops) — mitigated idiomatically by Class/Type tokens and the Signature attribute; (b) **performance** (boxing) — addressed structurally by Valhalla. - Note the surprising upside: erasure's 'one class file' keeps code size and class-loading lean, and reflection still recovers declared generic types, so most framework needs are met without reification. - Avoid absolutist claims ('erasure is bad'): each model is internally coherent for its platform's history.

  • Concretely, what does erasure cost at runtime for List<Integer> that C#'s List<int> avoids?
    Boxing: each int is wrapped in a heap-allocated Integer (object header + pointer indirection), hurting memory footprint and cache locality. C# specializes List<int> to store raw ints contiguously. This is the gap Valhalla's value classes and primitive generic specialization target.
  • Does Project Valhalla make Java generics fully reified?
    No. It adds value/primitive (identity-free, flattenable) classes and specialization of generics over primitives to remove boxing, while keeping erasure's compatibility and 'one class file per generic type' benefits. It is a performance fix, not a switch to C#-style runtime reification of reference-type arguments.

saying these in an interview costs you the question

  • Saying Valhalla reifies Java generics like C#
  • Calling erasure a 'mistake' without the 2004 compatibility context
  • Claiming reified generics have no runtime cost
  • Forgetting that boxing is erasure's main performance penalty
  • Asserting reflection can't see any generic info under erasure (it sees declarations)

context