Java added generics in Java 5, and the team made a call that still echoes today: type erasure. At compile time, List<String> and List<Integer> become the same List. The type parameter disappears. The JVM never sees it.
At the time, this made sense. There were millions of lines of Java in production. Modifying the JVM to carry type metadata at runtime would have broken binary compatibility across the entire ecosystem. Erasure was the safe path, and probably the right one.
But that was 2004.
The cost compounds
Here's the thing: the modern JVM ecosystem increasingly needs runtime type information. Jackson, Hibernate, Spring, Quarkus, Micronaut, Kafka serializers, Bean Validation, OpenAPI generators... they all depend on knowing what generic types are at runtime. And since the JVM threw that away, they spend enormous effort reconstructing it artificially.
The most obvious example is Jackson:
List<String> result = mapper.readValue(json, new TypeReference<List<String>>() {});
That {} at the end is an anonymous class. You're literally subclassing TypeReference<List<String>> so the compiler embeds the type in the class definition, which you then dig out via reflection. It works, but it's a workaround for something the runtime should just know. And in a large codebase you'll find hundreds of these.
Every year the ecosystem gets more sophisticated at reconstructing what erasure discarded. TypeReference, TypeToken, ResolvableType... the gap keeps growing.
The idea
So I started thinking: what if Java introduced a new kind of class that keeps its generic type at runtime?
reified class Box<T> {
private T value;
}
The key word here is new kind. The proposal isn't to change how existing generics work. It's to introduce a second system that coexists with the first:
- existing erased generics stay exactly as they are
-
reifiedclasses carry real type metadata at runtime - the boundary between both worlds is explicit and compiler-enforced
With this, things that are currently awkward become natural:
// no more TypeReference
Box<String>.class
// instanceof with real parameterized types
if (box instanceof Box<String> b) { ... }
// reflection that actually knows the type
box.getClass().getTypeArguments() // -> [String]
Prior art
This isn't a new problem, and other languages have tackled pieces of it.
Kotlin has inline fun <reified T>, which lets you use the type parameter at runtime inside a function. The compiler copies the function body at every call site and substitutes the concrete type there, similar to how Rust handles monomorphization. It works well, but only for functions:
inline fun <reified T> isInstance(value: Any) = value is T // works
class Box<T> {
fun getType() = T::class // doesn't compile
}
For classes with generic state, Kotlin hits the same wall as Java. The compiler trick doesn't scale there.
C# solved this properly, but they had the luxury of designing the CLR with reification from the start. List<string> and List<int> are genuinely different types at runtime. They also added explicit variance on interfaces with in and out:
interface IProducer<out T> { T Get(); } // covariant, T only comes out
interface IConsumer<in T> { void Use(T t); } // contravariant, T only goes in
Java's wildcards (? extends, ? super) cover similar ground, but you declare the variance at every usage site instead of once on the interface. The PECS rule (Producer Extends, Consumer Super) exists precisely because people keep forgetting which is which.
Project Valhalla is also working in this space, focused on specialized generics for value types. It's not exactly the same problem, but it overlaps.
Where things get complicated
Two type systems living in the same language creates real tension.
The most immediate issue is the boundary. A reified class can always degrade to a raw type if you need to cross into the erased world, but the reverse doesn't work. You can't reconstruct type information that was never stored. This makes the boundary unidirectional: reified -> erased is allowed (explicitly), erased -> reified is not.
That also makes cross-world inheritance essentially unviable:
// almost certainly can't be allowed
reified class ReifiedList<T> extends ArrayList<T> { ... }
ArrayList is erased. Inheriting from it means crossing the boundary inside the class definition, which is the worst place for that to happen implicitly. The cleaner model is two separate inheritance hierarchies that only touch through an explicit, controlled conversion.
Class loading is another hard problem. The JVM loads classes, not parameterized instantiations. Box<String> and Box<Integer> being distinct types at runtime means either a different classloading model or a metadata layer outside the traditional class system. C# did this from scratch. Retrofitting it onto 30 years of JVM infrastructure is a different problem entirely.
Reflection also breaks. java.lang.Class and java.lang.reflect.* have no concept of real parameterized types. Parts of the core API would need to change.
And bridge methods, generated by the compiler to handle erasure in inheritance, assume a single erased version of each generic class. With partial reification, the rules change in ways that aren't trivial.
Open questions
- Does a second type system make more sense than evolving the existing one?
- How do you define the exact rules for crossing the boundary in both directions?
- Is the dual-model complexity worth it, or is a more conservative approach inside the current system better?
- What other implementation problems am I not seeing?
Wrapping up
Java made the right call in 2004. Backward compatibility for a massive ecosystem is not a trivial concern, and type erasure let generics ship without breaking everything that existed.
But technical debt compounds. The workarounds are getting more sophisticated every year, and the frameworks that power most of the JVM ecosystem are increasingly built around reconstructing what the runtime threw away.
This post is a mental exercise in what an opt-in, backward-compatible path out of that debt could look like. I'm not a language designer, and I'm sure there are implementation problems I haven't thought of. That's kind of the point of writing it down.
Top comments (0)