Reputation: 15520
Say I have these classes:
class Container<T> {
private List<T> list;
}
And I have an instance of Container
, say
Container<?> instance = new Container<String>();
Is there a way to find out list
's actual type parameter at run-time? I.e., that it's a List
of String
, rather than a List
of java.lang.reflect.TypeVariable
.
I've been looking at Java's reflection (field.getGenericType()
), various libraries (Guava Reflect, Typetools, ClassMate, even ASM, etc.) but I can't figure it out.
I have no control over the Container
type, so I can't add any fields to it.
I realize it may not be possible at all due to erasure, although I don't mind messing with bytecode if that makes it possible.
EDIT I over-simplified my initial question. I have an additional piece of information available; I also have this type:
class StringContainerContainer {
private Container<String> container;
}
(And several more types like it, for different generic parameters.)
At run-time, I'm given the instance
object mentioned above. With a little refactoring of my codebase, I can also get to the StringContainerContainer
class (or whichever *ContainerContainer
class is relevant), which I can use to get a java.lang.reflect.Field
for container
.
Upvotes: 2
Views: 584
Reputation: 15372
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:
List<String>
has a raw type of List
and one argument that is String
.? extends Foo
.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.
Upvotes: 2
Reputation: 269777
With your edit, yes, you can find out that String
is the actual type parameter:
Field field = StringContainerContainer.class.getDeclaredField("container");
ParameterizedType gt = (ParameterizedType) field.getGenericType();
Class<?> arg = (Class<?>) gt.getActualTypeArguments()[0];
System.out.println(arg);
I should point out that this identifies the type argument of the container
field of StringContainerContainer
. Given how Container
is written, that also happens to be the type argument of its list
field, but that field isn't directly examined.
Upvotes: 3
Reputation: 234837
Due to type erasure, you can't do it. The most you can do with reflection is get information about the declared type variables, not the actual types bound by an instance at run time. That information is thrown away by the compiler.
For more info, read the tutorial on Type Erasure and/or section 4.6 of the Java Language Specification, which discusses type erasure.
If you need run-time type information about the instance class, the best you can do is require that a Class
object be passed at instance construction:
class Container<T> {
private List<T> list;
private Class<T> type;
public Container(Class<T> type) {
this.type = type;
}
}
Upvotes: 1