Type Erasure in Java

Generic type information exists only at compile time. Knowing what the compiler removes explains every generics restriction.

What erasure is

Generics were added in Java 5, and existing bytecode had to keep working. The solution was type erasure: the compiler checks types, then removes the type arguments and inserts casts where needed. At runtime there is no generic information left.

// What you write
List<String> names = new ArrayList<>();
names.add("Meera");
String first = names.get(0);

// Roughly what the bytecode holds
List names = new ArrayList();
names.add("Meera");
String first = (String) names.get(0);   // a cast the compiler proved is safe

The evidence

List<String> texts = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();

System.out.println(texts.getClass() == numbers.getClass());   // true
System.out.println(texts.getClass().getSimpleName());         // ArrayList

Both are the same class at runtime. The type argument existed only for the compiler.

How bounds are erased

DeclaredErased to
<T>Object
<T extends Number>Number
<T extends Comparable<T> & Serializable>Comparable, the first bound
List<String>List

The restrictions it causes

No runtime type test

// if (value instanceof List<String>)   // compile error
if (value instanceof List<?> list) { }  // the only form available

No new T()

public class Factory<T> {
    // public T create() { return new T(); }   // T does not exist at runtime
}

// The usual workaround: pass a supplier
public class Factory<T> {

    private final Supplier<T> supplier;

    public Factory(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T create() {
        return supplier.get();
    }
}

Factory<ArrayList<String>> factory = new Factory<>(ArrayList::new);

No generic arrays

// List<String>[] arrays = new List<String>[10];    // compile error

@SuppressWarnings("unchecked")
List<String>[] arrays = new List[10];               // legal but unchecked

Arrays check their element type at runtime, and generics have no runtime type to check. Mixing them is unsound, so the compiler forbids the direct form. Use List<List<String>> instead.

No overloads that erase to the same signature

public class Printer {
    // void print(List<String> items) { }
    // void print(List<Integer> items) { }   // both erase to print(List)
}

Generic exceptions are impossible

// class MyException<T> extends Exception { }   // not allowed

A catch clause matches types at runtime, and the type argument would be gone.

Static members cannot use the class parameter

public class Holder<T> {
    // private static T shared;              // not allowed
    // static void set(T value) { }          // not allowed

    static <U> void helper(U value) { }      // a method level parameter is fine
}

Heap pollution

List<String> texts = new ArrayList<>();
List raw = texts;                 // raw type, checks disabled
raw.add(42);                      // no complaint at this line

String value = texts.get(0);      // ClassCastException here, far from the cause

Heap pollution is a variable of a parameterised type referring to an object that is not of that type. Erasure makes it possible, and the failure surfaces at an unrelated line, which is why raw types are worth avoiding entirely.

Generic varargs

@SafeVarargs
public static <T> List<T> listOf(T... items) {
    return new ArrayList<>(Arrays.asList(items));
}

A varargs parameter becomes an array, and a generic array is unsound, so the compiler warns. @SafeVarargs asserts that the method only reads the array and never stores into it or lets it escape. It is allowed on static, final and private methods and on constructors.

What survives erasure

public class NoteRepository implements Repository<Note, Long> { }

Type type = NoteRepository.class.getGenericInterfaces()[0];
System.out.println(type);   // Repository<Note, java.lang.Long>

Type arguments used in a class or method declaration are kept in the class file as metadata, so reflection can read them. What is erased is the type of a value at runtime. This distinction is what lets frameworks discover a declared type while list.getClass() still cannot tell you its element type.

Getting a type at runtime, when you must

public class TypedBox<T> {

    private final Class<T> type;

    public TypedBox(Class<T> type) {
        this.type = type;
    }

    public T cast(Object value) {
        return type.cast(value);      // the class object carries the type
    }
}

TypedBox<String> box = new TypedBox<>(String.class);

Passing a Class object is the standard way to recover the type the compiler discarded. The library uses this pattern in methods such as Collections.checkedList.

Why erasure was chosen

  • Existing libraries kept working without recompilation.
  • Generic and non generic code could be mixed during the transition.
  • No new bytecode or JVM changes were required.

The cost is every restriction above. Some other languages keep type arguments at runtime, at the price of a compatibility break Java was unwilling to make.

Common mistakes

  • Expecting instanceof List<String> to work.
  • Trying new T() or new T[n].
  • Mixing raw and parameterised types and getting an exception far from the cause.
  • Overloading on methods that erase to the same signature.
  • Assuming reflection can report the element type of an existing list.

Best practices

  • Never use raw types.
  • Pass a Class<T> when the type is genuinely needed at runtime.
  • Use List<List<T>> rather than arrays of generic types.
  • Treat every unchecked warning as something to fix or to justify with a comment.
  • Use @SafeVarargs only when the method truly cannot pollute the heap.

Practice

  1. Show that List<String> and List<Integer> have the same runtime class.
  2. Explain why a generic class cannot extend Throwable.
  3. Reproduce heap pollution with a raw type and identify the line that actually throws.
  4. Rewrite a class that needs new T() so that it takes a Supplier<T>.
  5. Why can two methods differing only in List<String> and List<Integer> not coexist?

Conclusion

The compiler checks generics and then throws the type arguments away. Every restriction, no new T(), no generic arrays, no runtime type tests, follows from that one decision, and knowing it turns confusing errors into predictable ones.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Generics in Java

Generics let a class or method work with a type supplied by the caller, moving whole families of errors from runtime to compile time.

Read more
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.