Java Reification
Generics are known for their type safety, which is obviously a good thing in programming. However, it has a flip side. It is well-known that type erasure — the compiler process that preserves type safety — can complicate program logic. Now it's time to discuss another generics-related notion: reification.
What is reification
Type erasure only affects concrete types — other data types are not affected and preserve their type data in byte code. Types that save information about themselves during type erasure are called reifiable, while types whose information is erased are called non-reifiable. The term reification refers to the process of making certain type parameters available at runtime as well as at compile-time.
The two groups
Let's recall what types are replaced during type erasure and can be called non-reifiable. Non-reifiable types include:
- parameterized types like
<T>
, which are replaced byObject
. - bounded generics type or wildcards. For example,
<T extends Number>
and<? extends Number>
are replaced byNumber
.
Reifiable types are more extensive. They include:
- primitive types like
int
anddouble
. - non-parameterized types such as
String
,Number
and other non-generic classes. - more complicated reifiable types, which are technically equivalent to
Object
. The first is a raw type. It is a type that can be parameterized but is not. For instance, if classBox<T>
is declared asBox box = new Box()
then it's a raw type. The second is an unbounded wildcard type, for example,Box<?>
. It includes arrays whose component type is reifiable as well.
Non-reifiable limitations
The fact that non-reifiable types are not present at runtime leads to some limitations:
1) It is prohibited to create an instance of a non-reifiable type.
Suppose you need to create an instance of a class of T
type inside a parameterized class. It looks fine to call a generic constructor, however, it leads to a compilation error
class Box<T> {
private T instance;
public void init() {
instance = new T(); // compile-time error: Type parameter T cannot be instantiated directly
}
}
This limitation is reasonable since we have no way to guarantee that T
will implement any particular constructor.
2) Another limitation for a non-reifiable type includes using instanceof
operator.
class Box<T> {
...
public boolean isIntegerSuperType() {
return Integer.valueOf(0) instanceof T; // compile-time error: Illegal generic type for instanceof
}
}
This operation is prohibited since the run-time bytecode contains no information on non-reifiable types, making it impossible to verify whether an object is an instance of such a type.
3) Only reifiable types can extend java.lang.Throwable
.
Suppose that there is a generic class that extends Throwable
.
class MyException<T> extends Exception {}
Given this code, the compiler raises the message Generic class may not extend java.lang.Throwable
. To illustrate the problem, suppose that the compiler ignored this error and ran the following code:
try {
...
} catch (MyException<String> e) {
System.out.println("String");
} catch (MyException<Long> e) {
System.out.println("Long");
}
After type erasure, both caught types would be translated into a single parameterless MyException
type. As a result, we have a dilemma on how to handle MyException
– the program would not know which exception message to print. For this reason, any generic extensions of Throwable
are prohibited.
4) Creating an instance of an array requires a reifiable type. This limitation also relates to Varargs
, which translates parameters into an array.
Let's look at the signature of the <T> T[] toArray(T[] a)
method in the Collection
class. The main task of an array passed as an argument is to provide type information at runtime.
Remember that due to type erasure, the code
Collection<Integer> col = new ArrayList<Integer>();
Integer[] array = col.toArray(new Integer[0]);
is equivalent to:
Collection col = new ArrayList();
// col has no type parameter information at runtime.
// Which array type should we create inside toArray() method without a parameter?
Integer[] array = (Integer[]) col.toArray();
Since type erasure handles the type casting, it's perfectly fine to call this method in the following way:
Collection<Integer> col = ... initializing of this Collection
// toArray will create array of appropriate size
Integer[] array = col.toArray(new Integer[0]);
In this manner, using a reifiable type such as Integer preserves type information at runtime.
5) Casting to non-reifiable types usually results in a warning notifying the programmer that this practice may lead to exceptions.
Conclusion
Recognizing the distinction between non-reifiable and reifiable types can help you avoid errors when implementing generics and wildcards. Non-reifiable types have limitations that prohibit certain operations involving creating instances and arrays, using the instanceof
operator, and creating parameterized successors. In addition, casting to non-reifiable types may result in a loss of type safety.