Appearance
Type Erasure
Generics are a technique similar to "template code," and the implementation of generics can vary across different languages.
In Java, the implementation of generics is based on type erasure.
Type erasure means that the virtual machine knows nothing about generics; all the work is done by the compiler.
For example, when we write a generic class Pair<T>
, this is what the compiler sees:
java
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
However, the virtual machine is completely unaware of generics. This is the code executed by the VM:
java
public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
Thus, Java implements generics using type erasure, which leads to the following:
- The compiler treats type
<T>
asObject
. - The compiler performs safe casting based on
<T>
.
When using generics, the code we write is also what the compiler sees:
java
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
But the code executed by the virtual machine has no generics:
java
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
Therefore, Java's generics are enforced by the compiler at compile time, which always treats all type T
as Object
. However, when casting is necessary, the compiler automatically performs safe casting based on the type of T
.
Having understood Java's implementation of generics—type erasure—we can identify the limitations of Java generics:
Limitation 1: <T>
cannot be a primitive type, such as int
, because the actual type is Object
, and Object
cannot hold primitive types:
java
Pair<int> p = new Pair<>(1, 2); // compile error!
Limitation 2: It is impossible to obtain a Class
with generics. Consider the following code:
java
public class Main {
public static void main(String[] args) {
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1 == c2); // true
System.out.println(c1 == Pair.class); // true
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
Because T
is treated as Object
, when we obtain the Class
for Pair<String>
and Pair<Integer>
, we receive the same Class
, which is the class of Pair
.
In other words, all generic instances, regardless of the type of T
, return the same Class
instance when calling getClass()
, because after compilation they all become Pair<Object>
.
Limitation 3: It is impossible to check the type with generics:
java
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}
The reason is the same: there is no Pair<String>.class
, only the singular Pair.class
.
Limitation 4: Cannot instantiate type T
:
java
public class Pair<T> {
private T first;
private T last;
public Pair() {
// Compile error:
first = new T();
last = new T();
}
}
The above code cannot compile because the two lines in the constructor:
java
first = new T();
last = new T();
After type erasure, actually become:
java
first = new Object();
last = new Object();
As a result, creating new Pair<String>()
and new Pair<Integer>()
would both end up being Object
, and the compiler rightly prevents this type mismatch.
To instantiate type T
, we must rely on an additional Class<T>
parameter:
java
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
first = clazz.newInstance();
last = clazz.newInstance();
}
}
The above code uses the Class<T>
parameter and reflection to instantiate type T
. When using it, you must also pass Class<T>
, for example:
java
Pair<String> pair = new Pair<>(String.class);
Since we passed an instance of Class<String>
, we can instantiate a String
type using String.class
.
Improper Overriding of Methods
Sometimes, a seemingly correct method definition fails to compile. For example:
java
public class Pair<T> {
public boolean equals(T t) {
return this == t;
}
}
This is because the defined equals(T t)
method will actually be erased to equals(Object t)
, and this method is inherited from Object
. The compiler prevents the definition of a generic method that would become an override.
Renaming the method to avoid conflict with Object.equals(Object)
allows it to compile successfully:
java
public class Pair<T> {
public boolean same(T t) {
return this == t;
}
}
Generic Inheritance
A class can inherit from a generic class. For example, if the parent class's type is Pair<Integer>
, the child class can be defined as IntPair
like this:
java
public class IntPair extends Pair<Integer> {
}
When using it, since the subclass IntPair
does not have a generic type, it can be used normally:
java
IntPair ip = new IntPair(1, 2);
As mentioned earlier, we cannot retrieve the type T
from Pair<T>
, meaning given a variable Pair<Integer> p
, we cannot derive Integer
type from p
.
However, when the parent class is a generic type, the compiler must save the type T
(which is Integer
for IntPair
) in the subclass's class file; otherwise, the compiler would not know that IntPair
can only store and retrieve the Integer
type.
In the case of inheriting a generic type, the subclass can retrieve the generic type from the parent class. For example, IntPair
can access the generic type Integer
of its parent class. Retrieving the parent class's generic type involves somewhat complex code:
java
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // could be multiple generic types
Type firstType = types[0]; // get the first generic type
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
Because Java introduced generics, using Class
alone to identify types is no longer sufficient. In fact, the structure of Java's type system is as follows:
┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘
Summary
- Java's generics are implemented using type erasure.
- Type erasure imposes the following limitations on generics
<T>
:- Cannot be a primitive type, such as
int
. - Cannot obtain a class with generics, such as
Pair<String>.class
. - Cannot check the type with generics, such as
x instanceof Pair<String>
. - Cannot instantiate type
T
, such asnew T()
- Cannot be a primitive type, such as
Generic methods should avoid duplicate method definitions, for example: public boolean equals(T obj)
; Subclasses can access the generic type <T>
of their parent class.