Generics are known for their type safety, which is obviously a good thing. However, it has a flip side. As we’ve already discussed, 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 certain data types — other 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 by Object.
  • bounded generics or wildcards. For example, <T extends Number> and <? extends Number> are replaced by Number.

Reifiable types are more extensive. They include:

  • primitive types like int and double.
  • non-parameterized types such as StringNumber 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 class Box<T> is declared as Box 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.

Leave a Reply

Your email address will not be published.