I am using reflection to discover methods of classes and their superclasses, along with the types of method arguments and return values. This mostly works, but I'm having trouble with some specific generic cases. Suppose I have a base class:
package net.redpoint.scratch;
public class Base<E> {
public E getE() { return null; }
}
And a subclass:
package net.redpoint.scratch;
public class Derived extends Base<String> {}
Using reflection I can walk through the methods of Derived and get arg and return types (code omitted, but it works fine). However, I also want to know the inherited methods. Using code below, I can come very, very close. But getting the correct return type of getE() eludes me. I can get the generic type "E" but not the actual type "java.lang.String":
package net.redpoint.scratch;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
public class Scratch {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("net.redpoint.scratch.Derived");
Type superType = clazz.getGenericSuperclass();
if (superType instanceof ParameterizedType) {
ParameterizedType superPt = (ParameterizedType)superType;
Type[] typeArgs = superPt.getActualTypeArguments();
Type t0 = typeArgs[0];
// This is "java.lang.String"
System.out.println(t0.getTypeName());
Type superRawType = superPt.getRawType();
if (superRawType instanceof Class) {
Class superRawClazz = (Class)superRawType;
for (Method method : superRawClazz.getDeclaredMethods()) {
if (method.getName().equals("getE")) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof TypeVariable) {
TypeVariable tv = (TypeVariable)returnType;
// This is "E"
System.out.println(tv.getName());
// How do I associate this "E" back to the correct type "java.lang.String"
}
}
}
}
}
}
}
The output is:
java.lang.String
E
My question is, how do I find that "E" is actually "java.lang.String"? I suspect it has something to do with a lookup into the typeArgs[], but I don't see how to get there. PLEASE do not respond unless you've actually worked with generics reflection. I've seen a lot of posts with answers like "type erasure prevents this", which is not true.
I'd recommend using Guava for this. For example:
Type returnType =
new TypeToken<Derived>() {}
.resolveType(Base.class.getMethod("getE").getGenericReturnType());
See TypeToken.resolveType(Type).
The alternative is pretty complicated, but it's possible. You need to walk the hierarchy and map type variables to type arguments yourself.
Handling the most trivial case would be something like this:
static Type getArgument(TypeVariable<?> var,
ParameterizedType actual) {
GenericDeclaration decl = var.getGenericDeclaration();
if (decl != actual.getRawType())
throw new IllegalArgumentException();
TypeVariable<?>[] vars = decl.getTypeParameters();
for (int i = 0; i < vars.length; ++i) {
if (var == vars[i]) {
return actual.getActualTypeArguments()[i];
}
}
return null;
}
That sort of simplistic method will fail if you had something like this:
abstract class AbstractDerived<T> extends Base<T> {}
class Derived extends AbstractDerived<String> {}
In cases like that you need to first map E from Base to T from AbstractDerived and then map T to String, so the method has to recurse or iterate the supertypes. I have a more complicated example of something like this here, but that example is still wrong for a number of reasons.
Another hurdle you will run in to is that to return a new type with type variables replaced, you need to implement ParameterizedType, GenericArrayType and WildcardType yourself, or else use the undocumented sun.reflect classes.
All of that is to say you should really just use Guava which already handles that stuff, unless you're doing something like writing your own TypeToken for some reason.
The caveat to all of this, which it seems like you already know, is that all of this depends on an actual type argument being provided to the supertype in an extends clause (explicit or implicit as in an anonymous class). If you just do new Base<Double>() there's no way to recover the type argument at runtime.
The trick is, getting the type parameters from the raw class, and correlating that with the type variables one gets when analyzing the return types or type arguments:
public class Scratch {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("net.redpoint.scratch.Derived");
Type superType = clazz.getGenericSuperclass();
if (superType instanceof ParameterizedType) {
ParameterizedType superPt = (ParameterizedType)superType;
Type[] typeArgs = superPt.getActualTypeArguments();
Type superRawType = superPt.getRawType();
if (superRawType instanceof Class) {
Class superRawClazz = (Class)superRawType;
TypeVariable[] typeParms = superRawClazz.getTypeParameters();
assert typeArgs.length == typeParms.length;
Map<TypeVariable,Type> typeMap = new HashMap<>();
for (int i = 0; i < typeArgs.length; i++) {
typeMap.put(typeParms[i], typeArgs[i]);
}
for (Method method : superRawClazz.getDeclaredMethods()) {
if (method.getName().equals("getE")) {
Type returnType = method.getGenericReturnType();
if (returnType instanceof TypeVariable) {
TypeVariable tv = (TypeVariable)returnType;
Type specializedType = typeMap.get(tv);
if (specializedType != null) {
// This generic parameter was replaced with an actual type
System.out.println(specializedType.toString());
} else {
// This generic parameter is still a variable
System.out.println(tv.getName());
}
}
}
}
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With