Type Erasure in Java Generics and Its Limitations
Introduction
Generics are checked by the compiler - but at runtime, a
(Note - This tutorial completes the generics module, which starts with Introduction to Java GenericsClick to read tutorial: Introduction to Java Generics: Why and How.) What is type erasure? When generics were added in Java 5, millions of lines of existing code - and the JVM itself - already worked with plain collections such as
OUTPUT of the above code
Explanation of the code
OUTPUT of the above command
Look at instructions 8 and 13 of
Limitation 1 - no primitive type arguments
After erasure, a type parameter becomes
OUTPUT on compiling the above code
The usual workarounds are to let the caller supply what is needed - a factory, such as a
OUTPUT of the above code
Limitation 3 - no instanceof or casts with type arguments
Because
OUTPUT of the above code
The cast "succeeded", and the failure came one line later, at the cast the compiler inserted when
OUTPUT on compiling the above code
Give such methods different names -
OUTPUT of the above code
List<String> and a List<Integer> are exactly the same class. The compiler removes the type arguments after checking them, a process called type erasure. It explains several rules of generics which otherwise seem arbitrary. This tutorial explains type erasure in Java generics and its limitations. We will start with what erasure is and why Java uses it. We will then see what the compiler turns generic code into, and prove at runtime that the type arguments are gone. Next we will go through the limitations erasure causes - no primitives, no new T(), no generic arrays, no instanceof with type arguments, and no overloading by type argument - and the usual workarounds. Lastly, we will look at heap pollution and what @SafeVarargs means.(Note - This tutorial completes the generics module, which starts with Introduction to Java GenericsClick to read tutorial: Introduction to Java Generics: Why and How.) What is type erasure? When generics were added in Java 5, millions of lines of existing code - and the JVM itself - already worked with plain collections such as
List. To stay compatible, Java's designers implemented generics entirely in the compiler. The compiler -- checks all the type arguments and reports any misuse as a compile error;
- then erases them: each type parameter is replaced by its bound -
Objectfor<T>, orNumberfor<T extends Number>- andList<String>becomes justList; - and inserts casts where values come out of generic code, such as
(String) list.get(0).
Type arguments do not exist at runtime
//ErasureDemo.java
import java.util.ArrayList;
import java.util.List;
public class ErasureDemo {
static class Box<T> {
T value;
}
static class NumberBox<T extends Number> {
T value;
}
public static void main(String[] args) throws Exception {
List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println("strings class : " + strings.getClass().getName());
System.out.println("integers class : " + integers.getClass().getName());
System.out.println("same class? : " + (strings.getClass() == integers.getClass()));
System.out.println("Box.value type : " + Box.class.getDeclaredField("value").getType().getName());
System.out.println("NumberBox.value type : " + NumberBox.class.getDeclaredField("value").getType().getName());
}
}
strings class : java.util.ArrayList integers class : java.util.ArrayList same class? : true Box.value type : java.lang.Object NumberBox.value type : java.lang.Number
- Both lists are simply
java.util.ArrayListat runtime - there is noArrayList<String>class. - The field
T valueis stored as anObjectinBox, and as aNumberinNumberBox- each type parameter was replaced by its bound.
javap -c, which shows the bytecode, as described in How Java Code RunsClick to read tutorial: How Java Code Runs: Bytecode, Class Loader and JIT -
A generic call, before erasure
//CastInserted.java
import java.util.List;
public class CastInserted {
public static void main(String[] args) {
List<String> names = List.of("Tom");
String first = names.get(0);
System.out.println(first);
}
}
javap -c CastInserted
Compiled from "CastInserted.java"
public class CastInserted {
public CastInserted();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #7 // String Tom
2: invokestatic #9 // InterfaceMethod java/util/List.of:(Ljava/lang/Object;)Ljava/util/List;
5: astore_1
6: aload_1
7: iconst_0
8: invokeinterface #15, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
13: checkcast #19 // class java/lang/String
16: astore_2
17: getstatic #21 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_2
21: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
24: return
}
main(): the call is to List.get, which returns Object (Ljava/lang/Object;), followed by a checkcast to String that the compiler added - exactly the cast we would have written without generics.
Object (or its bound), and primitives such as int are not objects. So List<int> is not allowed; use List<Integer> and autoboxing, as shown in the introduction to genericsClick to read tutorial: Introduction to Java Generics: Why and How. For large amounts of numeric data, primitive arrays or IntStream avoid the boxing cost.
Limitation 2 - no new T(), and no new T[]
At runtime, generic code does not know what T is, so it cannot create an object or an array of it -
Creating objects and arrays of a type parameter
//CannotCreate.java
public class CannotCreate<T> {
T create() {
return new T();
}
T[] createArray(int size) {
return new T[size];
}
}
CannotCreate.java:4: error: unexpected type
return new T();
^
required: class
found: type parameter T
where T is a type-variable:
T extends Object declared in class CannotCreate
CannotCreate.java:8: error: generic array creation
return new T[size];
^
2 errors
Supplier<T> or a constructor reference, or a Class<T> object, sometimes called a type token - or to use a List<T> instead of an array.
Creating objects and arrays with a Supplier and a Class token
//Workarounds.java
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public class Workarounds {
static <T> List<T> fill(Supplier<T> factory, int count) {
List<T> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
result.add(factory.get()); // the supplier creates each T
}
return result;
}
@SuppressWarnings("unchecked")
static <T> T[] newArray(Class<T> type, int size) {
return (T[]) Array.newInstance(type, size); // the Class object knows the real type
}
public static void main(String[] args) {
List<StringBuilder> builders = fill(StringBuilder::new, 3);
System.out.println("created " + builders.size() + " " + builders.get(0).getClass().getSimpleName() + "s");
String[] names = newArray(String.class, 2);
names[0] = "Tom";
names[1] = "Harry";
System.out.println(names.getClass().getSimpleName() + " of length " + names.length + ": " + String.join(", ", names));
}
}
created 3 StringBuilders String[] of length 2: Tom, Harry
List<String> and List<Integer> are the same class at runtime, obj instanceof List<String> cannot be checked and does not compile. You can only test for the erased type - obj instanceof List<?> - and a cast such as (List<String>) obj is an unchecked cast which the JVM cannot verify. If the cast was wrong, the error appears later, where an element is used -
An unchecked cast fails later, not at the cast
//UncheckedCast.java
import java.util.List;
public class UncheckedCast {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
Object data = List.of(1, 2, 3); // really a List<Integer>
System.out.println("data instanceof List<?> : " + (data instanceof List<?>));
List<String> names = (List<String>) data; // unchecked - the JVM cannot verify it
System.out.println("Cast succeeded, size " + names.size());
String first = names.get(0); // the inserted checkcast fails here
}
}
data instanceof List<?> : true Cast succeeded, size 3 Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at UncheckedCast.main(UncheckedCast.java:12)
get() returned an Integer. Treat every "unchecked" compiler warning as a possible bug.
Limitation 4 - no overloading by type argument
Two methods which differ only in their type arguments have the same signature after erasure, so they cannot be overloaded -
Overloads that clash after erasure
//ErasureClash.java
import java.util.List;
public class ErasureClash {
static void print(List<String> names) { }
static void print(List<Integer> numbers) { }
}
ErasureClash.java:6: error: name clash: print(List<Integer>) and print(List<String>) have the same erasure
static void print(List<Integer> numbers) { }
^
1 error
printNames() and printNumbers() - as the JDK does with Collectors.toList() and toSet(). (The rules for overloading are in Method OverloadingClick to read tutorial: Method Overloading in Java with Examples.) Two more consequences: a class cannot have static fields of type T, because there is one static field for all Box<...> types, and generic classes cannot extend Throwable, because a catch block cannot tell MyException<String> from MyException<Integer>.
Heap pollution and @SafeVarargs
Heap pollution is the state where a variable of a generic type refers to an object of the wrong type - like the List<String> above, which really held integers. It can only happen through unchecked operations: raw types, unchecked casts, and generic varargsClick to read tutorial: Java Varargs: Variable-Length Arguments with Examples. A varargs parameter such as T... items is an array, and since a T[] cannot really be created, the compiler warns about "possible heap pollution". A method which only reads the array, and never stores into it or lets it escape, is safe - and can be marked @SafeVarargs to promise this and silence the warning for callers.
A safe generic varargs method
//SafeVarargsDemo.java
import java.util.ArrayList;
import java.util.List;
public class SafeVarargsDemo {
@SafeVarargs
static <T> List<T> listOf(T... items) { // only reads items, so it is safe
List<T> result = new ArrayList<>();
for (T item : items) {
result.add(item);
}
return result;
}
public static void main(String[] args) {
List<String> names = listOf("Tom", "Harry", "Ethan");
List<Integer> numbers = listOf(1, 2, 3);
System.out.println(names + " " + numbers);
}
}
[Tom, Harry, Ethan] [1, 2, 3]
List.of(), Arrays.asList() and Stream.of() are all declared @SafeVarargs for this reason. Use the annotation only when the method really is safe - it switches off a warning that is there to protect you.
Summary
In this tutorial we first saw that Java implements generics through type erasure: the compiler checks type arguments, replaces type parameters with their bounds, and inserts casts - for compatibility with pre-Java 5 code. We then proved at runtime that List<String> and List<Integer> are the same class, and found the inserted checkcast with javap. Next we went through the limitations erasure causes - no primitive type arguments, no new T() or new T[] (with Supplier and Class workarounds), no instanceof with type arguments, unchecked casts, and no overloading by type argument. Lastly, we looked at heap pollution and @SafeVarargs. This completes the generics module.Core Java Tutorials
Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Introduction to Java Generics: Why and HowClick to read tutorial: Introduction to Java Generics: Why and How Java Generic Classes, Interfaces and MethodsClick to read tutorial: Java Generic Classes, Interfaces and Methods Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples
Java Generics Wildcards: extends, super and PECSClick to read tutorial: Java Generics Wildcards: extends, super and PECS Introduction to Java Generics: Why and HowClick to read tutorial: Introduction to Java Generics: Why and How Java Generic Classes, Interfaces and MethodsClick to read tutorial: Java Generic Classes, Interfaces and Methods Bounded Type Parameters in Java GenericsClick to read tutorial: Bounded Type Parameters in Java Generics Java Math Class: Essential Methods with ExamplesClick to read tutorial: Java Math Class: Essential Methods with Examples