🚀AdvancedAdvanced Generics

Type Erasure and Advanced Generics

Understand type erasure, heap pollution, reifiable types, and advanced generic patterns like type tokens.

Overview

Java generics are implemented via type erasure: the compiler uses type parameters for checking at compile time, then removes them entirely from the bytecode. At runtime, List<String> and List<Integer> are both just List. This design preserved backward compatibility with pre-generics Java code, but it has consequences: you cannot use type parameters in instanceof checks, you cannot create generic arrays directly, and you cannot retrieve the type parameter of a generic class at runtime (without tricks). Understanding erasure explains many confusing behaviors: unchecked cast warnings, heap pollution with varargs, and why instanceof T does not compile.

Key Points

  • •Type erasure replaces type parameters with their bounds (or Object if unbounded) in bytecode
  • •List<String> and List<Integer> are the same Class object at runtime: List.class
  • •You cannot do instanceof List<String>; only instanceof List<?> or instanceof List
  • •Generic arrays are not directly creatable: new T[10] does not compile
  • •Heap pollution: a List<String> variable holding a List<Integer> at runtime
  • •@SuppressWarnings("unchecked") silences erasure-related compiler warnings
  • •Type tokens (passing Class<T>) let you recover type information at runtime
  • •Super type tokens using anonymous subclasses allow capturing parameterized types
  • •Reifiable types are types whose complete type info is available at runtime (arrays, raw types, primitives)

Examples

What type erasure removes and what remains

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();

        // At runtime, both are just ArrayList
        System.out.println(strings.getClass() == integers.getClass()); // true
        System.out.println(strings.getClass().getName()); // java.util.ArrayList

        // Cannot check parameterized type at runtime
        Object obj = strings;
        System.out.println(obj instanceof List<?>); // OK: wildcard
        // System.out.println(obj instanceof List<String>); // compile error

        // After erasure, this is actually allowed (unchecked cast)
        @SuppressWarnings("unchecked")
        List<Integer> heapPolluted = (List<Integer>) (Object) strings;
        strings.add("hello");
        // Reading the integer list would cause ClassCastException at the get() site
    }
}

Heap pollution is when a variable of a parameterized type refers to an object of a different parameterized type. The JVM cannot detect this until an actual cast occurs (at get() time, not at the add() time).

Type token pattern to recover runtime type

import java.util.*;
import java.lang.reflect.*;

public class TypedList<T> {
    private final Class<T> type;
    private final List<Object> list = new ArrayList<>();

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

    public void add(T element) { list.add(element); }

    public T get(int index) {
        return type.cast(list.get(index)); // safe cast with runtime check
    }

    public boolean isCompatible(Object obj) {
        return type.isInstance(obj);
    }

    public static void main(String[] args) {
        TypedList<String> sl = new TypedList<>(String.class);
        sl.add("hello");
        System.out.println(sl.get(0));                   // hello
        System.out.println(sl.isCompatible("world"));    // true
        System.out.println(sl.isCompatible(42));         // false
    }
}

Passing Class<T> as a constructor argument is the "type token" pattern. It gives the class access to runtime type information that erasure would otherwise remove. Jackson and Gson use this pattern for deserialization.

Generic method bridge and bounded wildcards in practice

import java.util.*;

public class Main {
    // Lower bounded wildcard: add elements to any list of Number or supertype
    static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }

    // Upper bounded wildcard: read from any list of Number or subtype
    static double sum(List<? extends Number> list) {
        double total = 0;
        for (Number n : list) total += n.doubleValue();
        return total;
    }

    public static void main(String[] args) {
        List<Integer> ints = new ArrayList<>();
        addNumbers(ints);
        System.out.println("Sum: " + sum(ints)); // 6.0

        List<Double> doubles = List.of(1.5, 2.5, 3.0);
        System.out.println("Sum doubles: " + sum(doubles)); // 7.0
    }
}

PECS: Producer Extends, Consumer Super. Use ? extends T when reading (producing values for you). Use ? super T when writing (consuming values you provide). This is why Collections.copy(dest, src) uses List<? super T> dest and List<? extends T> src.

Common Mistakes

  • Trying to create a generic array with new T[n]. This does not compile. Instead, cast from Object[]: (T[]) new Object[n], or use a List<T>.
  • Expecting instanceof to work with parameterized types. instanceof List<String> is a compile error. Use instanceof List<?> to check for any List, then cast with an unchecked warning.
  • Ignoring @SuppressWarnings("unchecked") warnings without understanding them. Suppressing the warning does not remove the heap pollution risk. Only suppress when you are certain the cast is actually safe.
  • Not understanding PECS: passing a List<Integer> where List<Number> is expected. List<Integer> is NOT a subtype of List<Number> due to invariance. Use List<? extends Number> to accept both.

Note

When you need to capture a parameterized type at runtime (e.g., to deserialize JSON into List<User>), use a super type token: new TypeReference<List<User>>(){} creates an anonymous subclass whose generic supertype is captured in bytecode and readable via getGenericSuperclass(). Jackson uses exactly this pattern.

Exercises

Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.

Loading editor…

Completed: 0 / 2 exercises

Knowledge Check

Question 1 of 4

At runtime, what is the class of a List<String> object?