Generics were added very late in Java (J2SE 5) and one of the challenges with it was to maintain backward compatibility. Type erasure is a technique that the language and runtime designers chose to overcome this problem. Here is a simple demonstration of type erasure in action.

import java.util.ArrayList;
import java.util.List;
 
public class Erasure {
    public static void main(String args[]) {
        List<String> ls = new ArrayList<String>();
        System.out.println(ls.getClass().getName());
 
        List l = new ArrayList<String>();
        System.out.println(l.getClass().getName());
 
        Object o = new ArrayList<String>();
        System.out.println(o.getClass().getName());
    }
}

The output is as follows

java.util.ArrayList
java.util.ArrayList
java.util.ArrayList

Querying an object instance does not reveal the generic types associated with the instance.

The not so obvious aspect of type erasures is that the erasures happen on object instances but do not happen on type declarations. Here is a sample code that shows the effect

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static java.lang.System.out;
 
class PokeMe{
	Map<Set<Integer>, List<List<String>>> l;
}
 
public class GT {
	public static void main(String args[]) throws Exception {
		Class<PokeMe> clazz = PokeMe.class;
		Field el = clazz.getDeclaredField("l");
		Type t = el.getGenericType();
        typeSniffer(t);
	}
 
    private static void typeSniffer(Type t) {
        typeSniffer(0, t);
    }
 
    private static void typeSniffer(int level, Type t) {
        final StringBuilder padding = new StringBuilder();
        for(int i = 0; i < level; i++) {
            padding.append('\t');
        }
 
        out.println(padding.toString() + t);
        if(t instanceof ParameterizedType) {
            ParameterizedType pt = (ParameterizedType) t;
            for(Type at: pt.getActualTypeArguments()) {
                typeSniffer(level + 1, at);
            }
        }
    }
}

Running this code produces the following result:

java.util.Map<java.util.Set<java.lang.Integer>, java.util.List<java.util.List<java.lang.String>>>
	java.util.Set<java.lang.Integer>
		class java.lang.Integer
	java.util.List<java.util.List<java.lang.String>>
		java.util.List<java.lang.String>
			class java.lang.String

This often comes as a surprise even to those who have been programming in Java for many years. The reason why this is different from the earlier example is that we are inspecting declarations of a class member. Contrary to popular opinion, type erasure does not occur in the case of any sort of declaration. This is true for the following:

  • members of a class
  • argument types of methods
  • class definition themselves (when they inherit from a base type has generics and the class in question constrains those values)

Having this ability gives great validation power to developers who build reflection based frameworks in general