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)
P.S. Webucator made a video about this blogpost, which you can watch as well.
“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)
P.S. Webucator made a video about this blogpost, which you can watch as well.
Obviously the type information is preserved it is just not used by the JRE. If the type information was not preserved and accessible via reflection you wouldn’t be able to use compiled generic classes in your programs.
I’m hoping at some point they’ll break compatibility with below Java 5 (or hopefully below Java 6) and add this into runtime checking.
Good to remind Java developers about this: runtime info is indeed syntactic sugar, implemented with “invisible” casts and bridge methods. But class definitions do retain all relevant information.
I wrote about this a while ago (“You’re not my type? Super Type Tokens to Rescue!” http://www.cowtowncoder.com/blog/archives/2009/01/entry_127.html); and also wrote Java ClassMate library (https://github.com/cowtowncoder/java-classmate) to help. The problem, I think, is not that type information is not retained, but rather that even though it is there, and IS accessible, using it is very very difficult in presence of unresolved type variables/parameters. There’s bit more on the usability part in “Why ‘java.lang.reflect.Type’ Just Does Not Cut It as Complete Type Definition” (http://www.cowtowncoder.com/blog/archives/2010/12/entry_436.html).
Java Generic methods and generic classes enable programmers to specify, with a single method declaration, a set of related methods.