On Java Generics and Erasure
“Generics are erased during compilation” is common knowledge (well, type parameters and arguments are actually the ones erased). That happens due to “type erasure”. But it’s wrong that everything specified inside the <..>
symbols is erased, as many developers are assuming. See the code below:
public class ClassTest { public static void main(String[] args) throws Exception { ParameterizedType type = (ParameterizedType) Bar.class.getGenericSuperclass(); System.out.println(type.getActualTypeArguments()[0]); ParameterizedType fieldType = (ParameterizedType) Foo.class.getField("children").getGenericType(); System.out.println(fieldType.getActualTypeArguments()[0]); ParameterizedType paramType = (ParameterizedType) Foo.class.getMethod("foo", List.class) .getGenericParameterTypes()[0]; System.out.println(paramType.getActualTypeArguments()[0]); System.out.println(Foo.class.getTypeParameters()[0] .getBounds()[0]); } class Foo<E extends CharSequence> { public List<Bar> children = new ArrayList<Bar>(); public List<StringBuilder> foo(List<String> foo) {return null; } public void bar(List<? extends String> param) {} } class Bar extends Foo<String> {} }
Do you know what that prints?
class java.lang.String
class ClassTest$Bar
class java.lang.String
class java.lang.StringBuilder
interface java.lang.CharSequence
You see that every single type argument is preserved and is accessible via reflection at runtime. But then what is “type erasure”? Something must be erased? Yes. In fact, all of them are, except the structural ones – everything above is related to the structure of the classes, rather than the program flow. In other words, the metadata about the type arguments of a class and its field and methods is preserved to be accessed via reflection.
The rest, however, is erased. For example, the following code:
List<String> list = new ArrayList<>(); Iterator<String> it = list.iterator(); while (it.hasNext()) { String s = it.next(); }
will actually be transformed to this (the bytecode of the two snippets is identical):
List list = new ArrayList(); Iterator it = list.iterator(); while (it.hasNext()) { String s = (String) it.next(); }
So, all type arguments you have defined in the bodies of your methods will be removed and casts will be added where needed. Also, if a method is defined to accept List<T>
, this T
will be transformed to Object (or to its bound, if such is declared. And that’s why you can’t do new T()
(by the way, one open question about this erasure).
So far we covered the first two points of the type erasure definition. The third one is about bridge methods. And I’ve illustrated it with this stackoverflow question (and answer).
Two “morals” of all this. First, java generics are complicated. But you can use them without understanding all the complications.
Second, do not assume that all type information is erased – the structural type arguments are there, so make use of them, if needed (but don’t be over-reliant on reflection).
Reference: | On Java Generics and Erasure from our JCG partner Bozhidar Bozhanov at the Bozho’s tech blog blog. |