A reifiable type is one whose type information is fully available at runtime. String is reifiable. String[] is reifiable. List<String> is not, because erasure removed the type argument before the class file was written. Every restriction below is the compiler refusing to write code whose safety depends on information that will not exist when the code runs.
The idea
The restriction list looks like seven unrelated rules and is really one rule applied seven times. A JVM operation that needs to know a type at runtime cannot be applied to a type argument, because the type argument is a compile-time fiction. Allocation needs a class. Array stores need an element type.
instanceofneeds a class. Acatchclause needs a class. Static fields need one type shared across all instantiations, and instantiations do not exist as separate entities. Read the list this way and it stops being trivia to memorize and becomes a single prediction you can make about any new generic construct you are considering.
Primitives are not type arguments
Pair<int, char> p = new Pair<>(8, 'a') does not compile. Only reference types can be type arguments, so you write Pair<Integer, Character> and accept the boxing.
This is downstream of erasure in a specific way. The erased class holds an Object field, and an int is not an Object, so there is nothing for the field to hold without a wrapper. A reifying runtime can specialize the class per primitive, which is what C# does and why List<int> there stores unboxed integers. The cost of Java’s answer is an allocation per element and a pointer chase per read, which is a real difference on a numeric workload and the motivation for the long-running work on value types.
You cannot instantiate a type parameter
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
}new compiles to a bytecode instruction naming a class. After erasure there is no class named E. The sanctioned workaround passes the class explicitly and uses reflection:
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}That Class<E> parameter is the seed of the type-token pattern, which is developed in Type Tokens and Super Type Tokens. The general move is to reintroduce, as an ordinary runtime value, exactly the type information erasure dropped.
You cannot declare a static field of a type parameter
public class MobileDevice<T> {
private static T os; // compile-time error
}The tutorial’s reasoning is the clearest statement of what a parameterized type is at runtime. A static field is shared across all instances of the class. Create a MobileDevice<Smartphone>, a MobileDevice<Pager>, and a MobileDevice<TabletPC>, and the question “what is the actual type of os” has three answers and one storage location. There is one class, so there is one static field, so it cannot have a per-instantiation type.
You cannot cast to or test a parameterized type
if (list instanceof ArrayList<Integer>) { // compile-time errorinstanceof requires a reifiable type. The check compiles to a runtime class comparison and there is no runtime distinction between an ArrayList<Integer> and an ArrayList<String>. The unbounded wildcard form is legal precisely because it asks a question the runtime can answer:
if (list instanceof ArrayList<?>) { // OKCasts follow the same rule from the other direction. (List<Number>) li on a List<Integer> is an error, while (ArrayList<String>) l1 on a List<String> is allowed, because in the second case the compiler can already prove the type argument matches and only the class is in question.
You cannot create an array of a parameterized type
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time errorThe reason is the most interesting one in the list, because it is about an existing runtime check rather than a missing one. Arrays are covariant and self-checking: every store into an array is verified against the array’s actual element type, and a bad store throws ArrayStoreException. That check is what makes covariant arrays tolerable, and it is explored in Covariant Arrays vs Invariant Generics.
The tutorial’s example shows what would happen if generic arrays were allowed:
Object[] stringLists = new List<String>[2]; // pretend this compiles
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // should throw ArrayStoreException,
// but the runtime cannot detect itThe array’s element type erases to List, both stores are List instances, and the runtime check passes on a store that violates the declared type. Java’s response was to forbid the array rather than let its one runtime type check silently become unsound.
You cannot create, catch, or throw a parameterized throwable
A generic class cannot extend Throwable, directly or indirectly, and a catch clause cannot name a type parameter:
class MathException<T> extends Exception { } // compile-time error
class QueueFullException<T> extends Throwable { } // compile-time error
catch (T e) { } // compile-time errorException dispatch is a runtime class match against the handler table in the class file, so a handler for T is a handler for nothing in particular. The throws clause is the exception to the exception: public void parse(File file) throws T is legal, because a throws clause is a compile-time declaration that nothing at runtime consults.
You cannot overload on erased-identical signatures
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { } // compile-time errorBoth erase to print(Set) and a class file cannot hold two methods with the same name and descriptor. This one has no runtime component at all. It is the class file format telling you what it can represent.
The predictive form
Before writing any generic construct, ask what the JVM would have to know at runtime for it to be safe. If the answer includes a type argument, the construct is either forbidden or unchecked. That single question replaces the memorized list, and it also tells you which workaround applies: pass the type as a
Classvalue, widen to an unbounded wildcard, or accept an unchecked cast and document why it is safe.
Related Notes
- Generics and Type Erasure in Java - the mechanism every restriction here derives from
- Type Tokens and Super Type Tokens - passing the erased type back in as a value
- Covariant Arrays vs Invariant Generics - why the array rule is about soundness rather than convenience
- Heap Pollution and Varargs - the hole erasure leaves open where the compiler cannot refuse
- Reified Generics in the CLR - the same seven questions on a runtime that keeps the type arguments
- Type Systems: Goals & Guarantees - what a type system is promising when it refuses a program
Sources
- “Restrictions on Generics,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/restrictions.html . Supports all seven restrictions and their stated reasons, including the
Pair<int, char>error, thenew E()error and theClass<E>reflection workaround, theMobileDevice<T>static field example and the three-instantiation argument, theinstanceof ArrayList<Integer>error and theArrayList<?>workaround, theList<Integer>[]error with thestringListsArrayStoreExceptionwalkthrough, theMathException/QueueFullException/catch (T e)errors with the legalthrows Texception, and theprint(Set<String>)/print(Set<Integer>)overload error. - “Type Erasure,” The Java Tutorials. https://docs.oracle.com/javase/tutorial/java/generics/erasure.html . Supports the claim that type parameters are replaced with their bounds or
Objectand that the produced bytecode contains only ordinary classes, interfaces, and methods.