Reified vs. Erased Generics: A Four-Language Comparison
Ask four different languages “what was T, once my code actually runs?” and you get four genuinely different answers. Each one is a real trade between flexibility and cost, not a mistake waiting to be fixed.
Write a generic container, a simple Box<T>, in Java, C#, Kotlin, and Swift, and all four compile without complaint. Ask a running program what T actually was, though, and the answers split apart immediately. Java has no idea. C# knows exactly, and has built a specialized version of the box just for that type. Kotlin, despite running on the same platform as Java, usually doesn’t know either, except in one narrow, specific case. Swift knows, sometimes for free and sometimes not, depending on which generics feature you reached for. None of these four answers is wrong. Each is a deliberate trade between how flexible a generic system feels to write and how much it costs to run.
Java: One Compiled Shape, No Memory of T
Java’s generics are fully erased. The compiler uses T to check your code for type mistakes, then throws that information away entirely before producing bytecode. A Box<String> and a Box<Integer> compile down to the exact same class, and the running JVM has no record that a type parameter was ever involved. That’s why instanceof Box<String> doesn’t compile in Java; there’s nothing left at runtime to check it against.
The upside is total backward compatibility. Java added generics in 2004, well after millions of lines of non-generic code already existed, and erasure let old and new code call each other without anyone recompiling anything. The downside is equally real: primitives can’t go into a generic collection without boxing first, since erasure replaces T with Object, and reflection-heavy frameworks need workarounds just to recover a type argument they should already know.
C#: Full Reification, Specialized Per Value Type
C# sits at the opposite end of the spectrum. The Common Language Runtime keeps type arguments around at runtime, and for value types specifically, things like int or a custom struct, it generates a genuinely separate, specialized implementation the first time it sees that type used with a given generic. A List<int> in C# stores real, unboxed ints in a real array, because the runtime built a dedicated version of List just for int. Reference types share more of the underlying implementation, since a pointer is a pointer no matter what it points to, but the type argument still survives; typeof(List<int>) genuinely tells you the element type at runtime.
This is the most expensive option to implement inside the runtime, and the most convenient one to write against. Nothing needs boxing. Reflection just works. The cost shows up elsewhere: the CLR has to generate and cache a specialized implementation for every distinct value-type instantiation your program actually uses, which is real, ongoing work the runtime does on your behalf.
Kotlin: Erasure by Default, Reification on Loan
Kotlin compiles to the same JVM bytecode Java does, so ordinary Kotlin generics are erased in exactly the same way. A List<String> and a List<Int> in Kotlin are, once compiled, the same erased class. There’s no free reification hiding underneath just because the syntax looks friendlier.
What Kotlin does offer is a narrow, specific escape hatch: the reified keyword, and it only works on type parameters of inline functions. Because an inline function’s body gets copied directly into the caller at compile time, the compiler knows the real type argument at each individual call site, and can bake it in before erasure ever gets the chance to remove it. This is a compile-time trick borrowed from inlining, not a runtime capability the JVM gained. It can’t be applied to a regular class’s type parameter, and it can’t be called from Java, because the whole mechanism depends on Kotlin’s own inliner rewriting the call.
Swift: A Different Answer for Every Feature
Swift is the most interesting case here, because it doesn’t give one answer, it gives two, depending on which generics feature you reach for, and that split is worth understanding in detail.
Generic functions: shared code, real metadata
An ordinary Swift generic function is compiled once, not once per type. Rather than throwing type information away like Java, Swift passes it along at runtime as hidden arguments: a pointer to the type’s metadata record and a witness table describing how to copy, allocate, and destroy values of that type. According to the Swift compiler team’s own generics documentation, this indirection genuinely costs something, extra memory accesses and indirect calls through the witness table, compared to code written by hand for one specific type.
Specialization: the same code, minus the indirection
Swift claws that cost back through an optimization called specialization. When the compiler can see a generic function’s actual body at the call site, either because it’s in the same module or explicitly marked @inlinable across module boundaries, it clones the function and substitutes the concrete type directly, eliminating the witness table indirection entirely. The result reads like C#’s per-type specialization, except it’s an optional optimization the compiler applies opportunistically, not a runtime guarantee that always happens.
Existentials: type erasure, opted into on purpose
Swift also has a second, separate mechanism, existential types, written as any SomeProtocol, that erases the concrete type deliberately. A value stored as any Drawable gets boxed into a fixed-size existential container alongside its witness table, and the compiler can no longer specialize anything against it, precisely because the concrete type has been hidden on purpose. Where Java’s erasure is the language’s only option, Swift’s erasure is a feature you explicitly opt into when you genuinely need runtime polymorphism over unrelated types, and you can move back to a specializable generic form whenever you don’t.
Where Each Language Lands on the Reification Spectrum
0 represents full erasure with no runtime type information; 10 represents full compile-time specialization with zero abstraction overhead. Swift spans nearly the whole range depending on which feature is used.

What Specialization Is Actually Worth, in Measured Numbers
It’s easy to describe specialization as “faster” in the abstract. A peer-reviewed measurement gives it a real number. Academic work on optimizing Swift’s protocol dispatch, published at OOPSLA 2019 and later folded into the official Swift compiler, measured the effect of removing existential-container overhead and enabling specialization across a defined set of real performance-sensitive code spans.
Measured Speedup From Removing Existential Overhead in Swift
Range of speedups observed across the performance-critical code spans measured in the study, after the compiler could specialize generic code instead of dispatching through witness tables.
That’s a wide range, six times more speedup in the best case than the worst, and that width is itself the lesson. Specialization isn’t a fixed, guaranteed discount. It’s a variable-sized win that depends entirely on how much witness-table indirection a given piece of code was paying for in the first place, which is exactly why Swift treats it as an optimization the compiler applies when it can, rather than a blanket promise.
Side by Side
| Question | Java | C# | Kotlin | Swift |
|---|---|---|---|---|
| Is T known at runtime by default? | No | Yes | No | Yes, via metadata |
| One compiled implementation, or many? | Always one | One per value-type instantiation | Always one | One by default, more if specialized |
| Can primitives/value types avoid boxing? | No | Yes | No | Yes, when specialized |
| Escape hatch for full reification | None | Not needed; it’s the default | reified, inline functions only | Specialization (automatic) or @_specialize (manual) |
| Escape hatch for erasure, if you want it | Not needed; it’s the default | Manual boxing to object | Not needed; it’s the default | any Protocol existentials |
The Real Trade: Who Pays, and When
Step back from the mechanics and the same underlying trade shows up in every one of these four designs. Someone has to pay for a generic system that’s both flexible and fast, and the only real choice is when. Java and Kotlin push the cost onto individual developers, in the form of manual boxing, workaround libraries, or a narrow inline-only keyword, in exchange for a genuinely simple runtime with no specialization machinery to maintain. C# pushes the cost onto the runtime itself, which has to generate and cache specialized code on the fly, in exchange for generic code that’s never slower than code you’d have written by hand. Swift splits the bill: ordinary generics pay a small, real indirection cost by default, existentials pay more because you asked for erasure explicitly, and specialization claws the cost back for the compiler, transparently, whenever it has enough information to do so.
Quick Checklist: Reasoning About Any Language’s Generics
- Ask whether the type argument survives compilation, or whether it’s purely a compile-time bookkeeping tool that gets discarded.
- Check whether primitives or value types need boxing to enter a generic container; that’s usually a direct symptom of erasure.
- Look for an escape hatch in either direction: Kotlin’s
reifiedrecovers type info, Swift’sanygives it up on purpose. - In Swift specifically, don’t assume all generic code is equally fast; check whether the compiler could actually specialize it, since witness-table dispatch and specialized code can differ by a wide, measured margin.
- Remember that none of these defaults are accidents. Each one reflects what the runtime looked like, and what problem needed solving, at the moment that language’s generics were designed.
What We Have Learned
Java erases generics entirely, trading runtime type information for a simple, backward-compatible runtime. C# reifies them fully, specializing per value type so nothing needs boxing, at the cost of runtime code generation machinery Java never needed to build. Kotlin inherits Java’s erasure by default and recovers it only through the narrow, inline-only reified keyword, a compile-time trick rather than a runtime capability.
Swift is the outlier that refuses to pick one answer: ordinary generics keep real runtime metadata and pay a modest, measured indirection cost, existentials erase the type on purpose when you explicitly ask for polymorphism, and specialization claws the cost back automatically, with a peer-reviewed range of 6.9 to 55.49 percent measured speedup once it kicks in. Four languages, one underlying question, and four defensible answers, because the actual trade was never “reification is better” or “erasure is better.” It was always about who pays the cost of flexibility, the runtime or the developer, and when.



