1. Overview
In this tutorial, we're going to get familiar with super type tokens and see how they can help us to preserve generic type information at runtime.
2. The Erasure
Sometimes we need to convey particular type information to a method. For example, here we expect from Jackson to convert the JSON byte array to a String:
byte[] data = // fetch json from somewhere String json = objectMapper.readValue(data, String.class);
We're communicating this expectation via a literal class token, in this case, the String.class.
However, we can't set the same expectation for generic types as easily:
Map<String, String> json = objectMapper.readValue(data, Map<String, String>.class); // won't compile
Java erases generic type information during compilation. Therefore, generic type parameters are merely an artifact of the source code and will be absent at runtime.
2.1. Reification
Technically speaking, the generic types are not reified in Java. In programming language's terminology, when a type is present at runtime, we say that type is reified.
The reified types in Java are as follows:
- Simple primitive types such as long
- Non-generic abstractions such as String or Runnable
- Raw types such as List or HashMap
- Generic types in which all types are unbounded wildcards such as List<?> or HashMap<?, ?>
- Arrays of other reified types such as String[], int[], List[], or Map<?, ?>[]
Consequently, we can't use something like Map<String, String>.class because the Map<String, String> is not a reified type.
3. Super Type Token
As it turns out, we can take advantage of the power of anonymous inner classes in Java to preserve the type information during compile time:
public abstract class TypeReference<T> { private final Type type; public TypeReference() { Type superclass = getClass().getGenericSuperclass(); type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; } public Type getType() { return type; } }
This class is abstract, so we only can derive subclasses from it.
For example, we can create an anonymous inner:
TypeReference<Map<String, Integer>> token = new TypeReference<Map<String, String>>() {};
The constructor does the following steps to preserve the type information:
- First, it gets the generic superclass metadata for this particular instance – in this case, the generic superclass is TypeReference<Map<String, Integer>>
- Then, it gets and stores the actual type parameter for the generic superclass – in this case, it would be Map<String, Integer>
This approach for preserving the generic type information is usually known as super type token:
TypeReference<Map<String, Integer>> token = new TypeReference<Map<String, Integer>>() {}; Type type = token.getType(); assertEquals("java.util.Map<java.lang.String, java.lang.Integer>", type.getTypeName()); Type[] typeArguments = ((ParameterizedType) type).getActualTypeArguments(); assertEquals("java.lang.String", typeArguments[0].getTypeName()); assertEquals("java.lang.Integer", typeArguments[1].getTypeName());
Using super type tokens, we know that the container type is Map, and also, its type parameters are String and Integer.
This pattern is so famous that libraries like Jackson and frameworks like Spring have their own implementations of it. Parsing a JSON object into a Map<String, String> can be accomplished by defining that type with a super type token:
TypeReference<Map<String, String>> token = new TypeReference<Map<String, String>>() {}; Map<String, String> json = objectMapper.readValue(data, token);
4. Conclusion
In this tutorial, we learned how we can use super type tokens to preserve the generic type information at runtime.
As usual, all the examples are available over on GitHub.