Skip to Content
Course content

65: Type Erasure Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing code from developers moving into advanced Java: they treat generics as if they are "baked into" the object at runtime. They assume that if they have a List<String>, the JVM knows, with absolute certainty, that it is a list of strings while the program is running. This leads to a lot of frustration when they try to perform runtime type checks and the compiler starts shouting at them.

The myth that List<String> is a distinct class at runtime

You might think that List<String> and List<Integer> are two different types of objects in memory. After all, they behave differently in your IDE and the compiler prevents you from putting an Integer into a String list. It feels natural to assume that if you can distinguish them at compile time, you can distinguish them at runtime.

Let's try to prove that. Look at this snippet:

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();

if (strings instanceof List<String>) { 
    // The compiler won't even let you write this line!
}

If generics were real runtime types, the instanceof operator would work perfectly. But Java won't let you do it. Why? Because by the time your code is actually running on the JVM, the <String> and <Integer> parts have been completely scrubbed away. This is Type Erasure.

What the compiler is actually doing behind your back

Java introduced generics in version 5. To make sure that new code could still work with old "legacy" code (binary compatibility), the engineers decided that generics should be a compile-time safety net, not a runtime feature.

When you compile your code, the Java compiler performs a "search and replace" operation. It replaces every type parameter (like T) with its bound—which is usually Object. Then, it inserts a cast wherever necessary to make sure the type safety is maintained.

Imagine you wrote this simple generic wrapper:

public class Box<T> {
    private T content;
    public void set(T content) { this.content = content; }
    public T get() { return content; }
}

To the JVM, that class actually looks like this:

public class Box {
    private Object content;
    public void set(Object content) { this.content = content; }
    public Object get() { return content; }
}

When you call box.get() in your code, the compiler automatically inserts a cast: (String) box.get(). You don't see it in your source code, but it's happening in the bytecode. I've always found it helpful to think of generics as "compile-time sugar." They make your life easier and your code safer, but they vanish the moment the .class file is generated.

The consequences of the disappearing act

Because of erasure, there are a few things you simply cannot do in Java. You cannot instantiate a generic array (new T[10] is illegal) and you cannot create an instance of a type parameter (new T() won't work). The JVM doesn't know what T is, so it doesn't know how much memory to allocate or which constructor to call.

If you absolutely need to know the type at runtime, you can't rely on the generic signature. You have to pass the class explicitly as a parameter—something we call a "Type Token." I'll show you how to do that in the exercise, but just remember: if you're wondering why you can't use instanceof with a generic type, it's because that type literally doesn't exist anymore.




📋 Practical Task

Implementing a Type-Safe Registry using Class Tokens

Since Type Erasure prevents us from using instanceof T, we have to use a Class<T&gt object to "remember" the type at runtime. Your task is to build a simple TypeRegistry that stores a single instance of various classes.

Requirements:

  • Create a class TypeRegistry.
  • Use a Map<Class<?>, Object> to store the registered instances.
  • Implement a method <T> void register(Class<T> type, T instance) that adds an object to the map using its class as the key.
  • Implement a method <T> T get(Class<T> type) that retrieves the object and casts it to the correct type.
  • In your main method, register a String and an Integer, then retrieve them. Verify that you can call String-specific methods (like .length()) on the result of get(String.class) without needing an explicit cast in your main logic.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.