There is always a lot of confusion around generics and erasure. Erasure means that the actual runtime objects are void of generic information. However, the reflection in Java does expose whenever generic information is recorded in the types. To carry the generic information, Java reflection has the Type
marker interface. Implementations are:
- Class – Each class is a Type
- ParameterizedType – Links the raw type to a set of argument Type objects. For example,
List<String>
has a raw type of List
and one argument that is String
.
- GenericArrayType – Records the component Type of an array
- WildcardType – Handles declarations like
? extends Foo
.
- TypeVariable – Handles references to type variables. For example,
class A<X> extends Foo<X> {}
, the X
in the declaration of Foo
will be represented as a TypeVariable
.
When generics were introduced, all reflective methods that could provide a Class, were augmented with a method that could provide a Type. For example, a Class has a method Class<?> Class.getSuperType()
, which is super confusing since it returns a Class object. There was a new method added, Type Class.getGenericSuperType()
Java lacks syntax support for a Type, there is no simple way to create a Type object in the language. The only way that I know to get a random Type is using a hack.
| Welcome to JShell -- Version 17.0.3
jshell> import java.lang.reflect.*;
jshell> public abstract class TypeReference<T> {}
| created class TypeReference
jshell>
Now in code we can do this:
jshell> ParameterizedType supertype = (ParameterizedType) new TypeReference<List<String>>() {}.getClass().getGenericSuperclass();
...> Type typeref = supertype.getActualTypeArguments()[0];
supertype ==> TypeReference<java.util.List<java.lang.String>>
typeref ==> java.util.List<java.lang.String>
jshell> Type typeref = supertype.getActualTypeArguments()[0];
typeref ==> java.util.List<java.lang.String>
jshell>
The typeref is in the given example List<String>
. We can move this code to the TypeReference class:
jshell> public abstract class TypeReference<T> {
...> public Type getType() {
...> ParameterizedType supertype = (ParameterizedType)
...> new TypeReference<List<String>>() {}
...> .getClass().getGenericSuperclass();
...> return supertype.getActualTypeArguments()[0];
...> }
...> }
This code looks a tad scary with the wild cast & array indexing. However, this code cannot fail, it is hard coded in the types we use, Java just lacks the power to do this in a syntactically clear way. However, when you have a loose Type
, you will need to do some parsing using instanceof
and lots of recursing.
The hard one in the parsing is the TypeVariable
. A TypeVariable
refers to a GenericDeclaration
. A Class
is a GenericDeclaration
, it represents the formal type parameters, i.e. the TypeVariable. For example, class Foo<X,Y> {}
declares two TypeVariables X and Y. Don't be seduced by the name variable, it is only the formal declaration.
public interface GenericDeclaration extends AnnotatedElement {
public TypeVariable<?>[] getTypeParameters();
}
So when you get a ParameterizedType
that has a TypeVariable
argument, you need to know the current invocation. This is really tricky and cannot always be done properly. Lets have:
jshell> class B<T> {}
...> class A<X> extends B<X> {}
...> A<String> a = new A<String>() {};
...> TypeVariable X = A.class.getTypeParameters()[0];
...>
| created class B
| created class A
a ==> 1@2b98378d
X ==> X
We created an anonymous subclass to record the generic reference to the super class A. This allows us to fetch the generic declaration.
jshell> ParameterizedType atype = (ParameterizedType) a.getClass().getGenericSuperclass();
...>
atype ==> A<java.lang.String>
This atype
now gives us a current definition for the X
type variable. Lets store it.
jshell> Map<TypeVariable,Type> vars = new HashMap<>();
...> vars.put(X, atype.getActualTypeArguments()[0] );
...>
vars ==> {}
$13 ==> null
So now we can get the generic super type of A
ParameterizedType btype = (ParameterizedType) a.getClass().getSuperclass().getGenericSuperclass();
...> Type T = btype.getActualTypeArguments()[0];
...> assert T instanceof TypeVariable;
...> Type actualT = vars.get((TypeVariable)T);
...> assert actualT == String.class
btype ==> B<X>
T ==> X
actualT ==> class java.lang.String
If you need to parse a complex type graph you will therefore need to maintain the current values of the type variables while you traverse it. I think you can run into situations where you would need to have multiple values for a Type Variable but have not spent the time to come up with an example.
Anyway, in your example you have the Container class. So you can get the field and then the generic declaration (casts omitted).
ParameterizedType type = Container.class.getField("list").getGenericType();
Class listtype = type.getActualArgumentType()[0];
Understanding the Java generic types is incredibly powerful. More than 10 years ago I created a Converter class in bnd that used generics to convert anything to anything if possible. A byte array to a List<Double>
, no problem! This became one of my most heavily used classes, cutting down boiler plate code to peanut size. Many other problems can also be reduced to a fraction of the code size.
Although the reflection support of the JVM is impressive, there is a continuous pain that the Java language completely ignores it. The lack of type literals, the problem we solved with the TypeReference class, and references to methods, constructors, and fields is so puzzling. We now have very syntax like Foo::a
to refer to a method but there is no known way to get the class & method back. And it is imho really hard to get any useful documentation. Really sad.