Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
65: Type Erasure Explained
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> 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
mainmethod, register aStringand anInteger, then retrieve them. Verify that you can callString-specific methods (like.length()) on the result ofget(String.class)without needing an explicit cast in your main logic.
There are no comments for now.