Generics In Depth
Java Fundamentals used generics constantly — List<String>, List<Integer> — without ever asking what actually happens to that type information once compiled. This chapter answers that, and it's where Java's generics genuinely part ways with C++ and Rust.
Generic Classes & Methods — A Quick Recap
A generic class or method is parameterized by one or more type variables — T here — filled in with a real type at each use site: new Box<String>(), new Box<Integer>().
Type Erasure — Java's Real Strategy
Generic type information is used only at compile time, for type checking — the compiler then erases it. At runtime, List<String> and List<Integer> are the exact same class, running the exact same bytecode, with no trace of which type argument was ever used.
This is genuinely different from both languages already covered on this site. cpp2-1's C++ templates and rust2-3's Rust generics both use monomorphization: the compiler generates a fully separate, specialized copy of the code for every distinct concrete type actually used — a real Box<String>-shaped set of machine instructions, entirely distinct from a real Box<int>-shaped one. Java never does this. One Box class, one set of bytecode, for every type argument that's ever used with it.
Why Type Erasure Exists
Generics arrived in Java 5, added on top of a bytecode format and roughly a decade of already-compiled, already-deployed code that had no concept of generics at all. Erasure was the deliberate trade-off that made this possible without breaking anything: generic code compiles down to the same bytecode shape pre-generics code always used, so old and new code can interoperate freely. The cost is exactly the runtime type information monomorphization would have preserved.
The Real Consequences of Erasure
Because the type argument doesn't exist at runtime, a generic class can't construct a new instance of its own type parameter, can't build an array of it, and code can't check which type argument a generic object was created with — only that it's some List, never specifically a List<String>.
Bounded Type Parameters
T extends Comparable<T> restricts T to types that implement Comparable, letting the compiler verify a.compareTo(b) is actually legal — narrowing what a type parameter can be still buys real compile-time checking, even with erasure in play afterward.
| Aspect | C++ templates (cpp2-1) | Rust generics (rust2-3) | Java generics |
|---|---|---|---|
| Strategy | monomorphization | monomorphization | type erasure |
| Runtime type info | full — distinct compiled code per type | full — distinct compiled code per type | none — one shared implementation |
| Compiled code size | grows with each distinct type used | grows with each distinct type used | fixed — one copy regardless of type args |
| Can hold a raw primitive | yes — real int stored directly | yes — real i32 stored directly | no — must box (java1-2) |
<T> only guarantees T is some reference type — bounding it with extends lets the compiler verify real method calls on T at compile time, recovering some of what erasure otherwise gives up.
obj instanceof List<String> does not compile — there is no String left to check against once the class is loaded. Only the raw type (List) survives erasure; the type argument is gone by the time any instanceof check could run.
Coding Challenges
Write code that creates a List<String> and a List<Double>, then prints whether their getClass() values are equal, explaining the result in a comment in terms of type erasure.
📄 View solutionWrite a generic class Pair<T> and attempt to add a method inside it that does `T value = new T();`. Show the resulting compile error and explain why erasure is the root cause.
📄 View solutionWrite a bounded generic method smaller(T a, T b) that requires T to implement Comparable<T>, returning whichever argument is smaller, and demonstrate it working with both Integer and String.
📄 View solutionChapter 1 Quick Reference
- Type erasure: generic type info is used for compile-time checking only, then discarded — one shared class/bytecode for every type argument
- C++ (cpp2-1) and Rust (rust2-3) instead use monomorphization — a real, distinct compiled copy per concrete type
- Consequences of erasure: no `new T()`, no `new T[]`, no `instanceof List<String>` — only the raw type survives
- Bounded type parameters (`T extends X`) recover real compile-time checking despite erasure
- Java generics still can't hold raw primitives — boxing (java1-2) is unavoidable, unlike C++/Rust's monomorphized code
- Next chapter: Collections Framework II — Map, Set, and Queue in depth