Consequences of Type Erasure
Type erasure is a design choice with concrete consequences — all of which are frequent Tripos topics.
1. Cannot Use Primitive Type Arguments
Generics work with reference types only. T is erased to Object (or its bound), and primitives are not objects:
List<int> numbers = new ArrayList<>(); // COMPILE ERROR
You must use the wrapper class instead, relying on autoboxing:
List<Integer> numbers = new ArrayList<>();
numbers.add(42); // autoboxed to Integer
int x = numbers.get(0); // auto-unboxed to int
Autoboxing has a performance cost — each primitive becomes a heap-allocated wrapper object, and each access boxes or unboxes. For performance-sensitive code with many elements, this overhead can be significant. Specialised primitive collections (e.g., Trove, FastUtil, or the IntStream API) exist to work around this limitation.
2. Creation of Generic Type Instances Is Forbidden
Inside a generic class, you cannot create objects or arrays of the type parameter:
class Factory<T> {
T create() {
return new T(); // COMPILE ERROR — T does not exist at runtime
}
T[] createArray(int n) {
return new T[n]; // COMPILE ERROR — same reason
}
}
At runtime, T has been erased to Object — the JVM has no idea what concrete class to instantiate.
Workaround 1: Pass a Class<T> object
class Factory<T> {
private Class<T> type;
Factory(Class<T> type) { this.type = type; }
T create() throws ReflectiveOperationException {
return type.getDeclaredConstructor().newInstance();
}
}
Factory<String> f = new Factory<>(String.class);
Workaround 2: Pass a Supplier<T>
class Factory<T> {
private Supplier<? extends T> supplier;
Factory(Supplier<? extends T> supplier) { this.supplier = supplier; }
T create() { return supplier.get(); }
}
Factory<String> f = new Factory<>(() -> new String("default"));
This is cleaner, avoids reflection, and is compile-time safe.
Workaround 3: For arrays, accept an existing array
T[] toArray(T[] a) { ... } // pattern used by Collection.toArray(T[])
The caller provides the array, and the method fills it. The caller’s array already has a concrete runtime type.
3. Overloading on Generic Parameters Is Impossible
Two methods whose signatures differ only in a generic type parameter have the same erased signature:
class Overloaded {
void process(List<String> list) { ... }
void process(List<Integer> list) { ... } // COMPILE ERROR
}
After erasure, both become void process(List list) — a duplicate method. The compiler rejects this.
This is why the standard library uses method naming conventions instead: Collections.sort(List<T>) uses Comparable for natural ordering and a separate Comparator for custom ordering, rather than overloading based on the type parameter.
4. No Runtime Type Check of Generic Parameters
if (collection instanceof List<String>) { ... } // COMPILE ERROR
if (collection instanceof List<?>) { ... } // OK — wildcard
if (collection instanceof List) { ... } // OK — raw type (with warning)
Only the raw type or an unbounded wildcard is checkable. List<String> is illegal because the JVM cannot distinguish List<String> from List<Integer> — they are the same class.
Similarly, you cannot catch a parameterised exception type:
try { ... }
catch (MyException<String> e) { ... } // COMPILE ERROR
The JVM’s exception-handling mechanism has no way to determine the erased type of the caught exception.
5. Arrays of Generic Types Are Problematic
List<String>[] array = new List<String>[10]; // COMPILE ERROR with generic array creation
This produces an unchecked warning because arrays are reified — they remember their component type at runtime — but generic type parameters are erased, creating an irreconcilable mismatch.
To understand why this is unsafe:
List<String>[] array = new List[10]; // unchecked — but let's say it's allowed
Object[] objects = array; // array covariance — List<String>[] is Object[]
objects[0] = new ArrayList<Integer>(); // ArrayStoreException? No — at runtime it's List[]
array[0].get(0); // ClassCastException — Integer is not a String
The array’s runtime type is List[] — it cannot distinguish List<String> from List<Integer>. The ArrayStoreException that normally protects against storing the wrong type in an array cannot help here, because at runtime both are just List.
Rule: prefer collections over arrays for generic types:
List<List<String>> listOfLists = new ArrayList<>(); // type-safe
This compiles cleanly, is fully type-safe, and avoids all the array-variance issues.