Dear Diary: Generics at runtime?
Dear Diary,
"Hobbits really are amazing creatures. You can learn all that there is to know about their ways in a month, and yet after a hundred years they can still surprise you"[1]. The astonishment Gandalf experiences, that you know something very well but somehow overlooked it, is not unfamiliar to me. Well, let me tell you a story…
Last week, a colleague of mine was refactoring code that uses Jackson’s TypeReference. The code did look like:
public abstract class JsonMapper<T extends EventBody> {
public abstract TypeReference<Event<T>> getTypeReference();
}
public class ProperEventMapper extends JsonMapper<ProperEventBody> {
// some mapper stuff
@Override
public TypeReference<Event<ProperEventBody>> getTypeReference() {
return new TypeReference<Event<ProperEventBody>>() {};
}
}
// a lot more mapper classes which all extended the JsonMapper
When he showed me that piece of code, I was like, this code can be written better.
Let’s remove the getTypeReference
implementations and move the implementation to the abstract class:
public abstract class JsonMapper<T extends EventBody> {
public TypeReference<Event<T>> getTypeReference() {
return new TypeReference<>() {}; // diamond operator for shortness
}
}
public class ProperEventMapper extends JsonMapper<ProperEventBody> {
// some mapper stuff
// no longer the `getTypeReference` here
}
Though it did compile without problems, the unit tests started to throw null pointers all over the place. When we put the code back to the original implementation, all test succeeded again! What was going on? I didn’t get it, so I started researching a bit.
The JavaDoc of the TypeReference class mentioned:
This generic abstract class is used for obtaining full generics type information by sub-classing
In other words, because the JVM removes all generic types at runtime[2], this class magically makes it possible to retrieve it anyway. I had no idea how this worked, but obviously strange things had to happen for it to work. I looked at the implementation of the TypeReference to get more insight:
public abstract class TypeReference<T> {
protected final Type _type;
protected TypeReference() {
final var superClass = (ParameterizedType) this.getClass().getGenericSuperclass();
this._type = superClass.getActualTypeArguments()[0];
}
public Type getType() {
return this._type;
}
}
Uhm what, so the generic type is available at runtime via reflection? How can this be? Is the type-erasure-story not true after all? I couldn’t imagine that to be true, so I clearly missed something.
A lot of Googling en ChatGPTing later, I finally learnt the answer!
Generic types are removed at runtime, but are available at compile time[3].
I learnt the generic types are stored in the Signature
attributes of a class file.
So I put it to the test.
I compiled the TypeReference
class and looked at the disassembled file with the Java Class File Disassembler:
> javap -v TypeReference.class
public abstract class TypeReference<T extends java.lang.Object> extends java.lang.Object
flags: (0x0421) ACC_PUBLIC, ACC_SUPER, ACC_ABSTRACT
this_class: #24 // TypeErasure/TypeReference
super_class: #2 // java/lang/Object
Constant pool:
..
#32 = Utf8 Signature
#33 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object;
..
{
protected final java.lang.reflect.Type _type;
descriptor: Ljava/lang/reflect/Type;
flags: (0x0014) ACC_PROTECTED, ACC_FINAL
protected TypeErasure.TypeReference();
descriptor: ()V
flags: (0x0004) ACC_PROTECTED
Code: ..
public java.lang.reflect.Type getType();
descriptor: ()Ljava/lang/reflect/Type;
flags: (0x0001) ACC_PUBLIC
Code: ..
}
Signature: #33 // <T:Ljava/lang/Object;>Ljava/lang/Object;
Aha, the generic type information is stored in the constant pool of the class.
But this does not give the final answer, because type T
is nowhere near a concrete type like Event<ProperEventBody>
.
But luckily ChatGPT gave me another clue.
Concrete types written in Java code are in the class file as well.
For example
public class StringList extends ArrayList<String> {}
has a signature of:
> javap -v StringList.class
public class StringList extends java.util.ArrayList<java.lang.String>
..
Signature: #12 // Ljava/util/ArrayList<Ljava/lang/String;>;
And now, the final revelation!
In our original code we wrote new TypeReference<Event<ProperEventBody>>() {}
.
This means you create a new implementation of the abstract class and instantiate it at the same time[4].
This new implementation is just a way to write shorter code, the compiler actually creates for every anonymous class an inner class.
When we disassemble the ProperEventMapper class we can see it:
> javap -v ProperEventMapper.class
public class ProperEventMapper extends some.package.JsonMapper<some.package.ProperEventBody>
..
Constant pool:
..
#21 = Class #22 // ProperEventMapper$1
#22 = Utf8 ProperEventMapper$1
#23 = Methodref #21.#3 // ProperEventMapper$1."<init>":()V
..
NestMembers:
ProperEventMapper$1
InnerClasses:
#21; // class ProperEventMapper$1
The signature of the nested classes are not in the javap output.
This is because every nested class does have its own class file.
Sadly enough, if you try javap -v ProperEventMapper$1.class
, the disassembler shows the result of the outer class.
But, by using some other terminal tools, we can still find the signature:
> cat ProperEventMapper$1.class | strings | grep -A 1 Signature | tail -n +2
Lcom/fasterxml/jackson/core/type/TypeReference<Lsome/package/Event<some/package/ProperEventBody;>;>;
And so the riddle of generic type at runtime is solved.
In the end, Jackson’s TypeReference is nothing more than a clever hack that has its own drawbacks.
When we moved the getTypeReference
to the abstract class, the concrete types like ProperEventBody
were no longer in the class files.
As only Event<T>
remains, Jackson cannot know the real type, thus it returns null as the result class.
By going back to the original implementation, we re-added the concrete types to the class files, so everything worked again!
SU,
Jacob
Jackson is not the only framework that uses this trick. Gson, for example, has a TypeToken class and Spring has its own ParameterizedTypeReference class. |