3

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.

4
  • And just for the record: your code works with Java8. I am not sure it would work with older versions of Java. Commented Jul 31, 2017 at 14:44
  • Good point. I switched to java 7, and found that Type.getTypeName() doesn't exist, using toString() instead is fine. Everything else seems the same. Commented Jul 31, 2017 at 14:52
  • For the record: please see the updates I did to my answer. I think you should be easily able to map E, F, ... abstract types to the corresponding specific type - with just a few lines of code! Commented Jul 31, 2017 at 16:53
  • And maybe it could be helpful to explain the problem that you actually want to solve here. What exactly is the use case of doing this reflection based type lookup? Commented Jul 31, 2017 at 17:04

3 Answers 3

5

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.

Sign up to request clarification or add additional context in comments.

5 Comments

Thanks! I was heading down the path of doing all of this by hand, but Guava looks a lot easier.
One note: Java 8 and later!
Ouch. Not sure that Guava can be used in my context. I don't have classes T to specialized TypeToken<T>, because I get the classes through reflection. And this doesn't work: TypeToken tt = new TypeToken(clazz) {}; It fails with "TypeToken isn't parameterized".
Ah, its TypeToken.of(clazz)
It turns out, I can use this with Java 7 after all. I had to get an older version of Guava.
4

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());
              }
            }
          }
        }
      }
    }
  }
}

1 Comment

Note: Even though my answer illustrates the fundamental issues around getting these types, @Radiodef solution with Guava is far superior IMHO.
0

There is a mis-conception on your end. The signature of getE() is defined in the base class already.

Derived doesn't override or in any way modify that method. In other words: the method is defined to return E. The fact that you extend Base and give a specific type doesn't affect the definition of that method!

Therefore your attempts to get to the "changed" return type are futile. Because the return type didn't change. And for correlating - look here:

    Class superClazz = Class.forName("Derived").getSuperclass();
    for (TypeVariable clz : superClazz.getTypeParameters()) {
        System.out.println(clz);
    }
    Class clazz = Class.forName("Derived");
    Type superType = clazz.getGenericSuperclass();
    if (superType instanceof ParameterizedType) {
        ParameterizedType superPt = (ParameterizedType) superType;
        for (Type t : superPt.getActualTypeArguments()) {
            System.out.println(t.getTypeName());
        }
    }

The above prints:

E
F
java.lang.String
java.lang.Integer

(for my reworked example where I used two parameters E, F and had them replaced with String, Integer)

9 Comments

Sigh. This is exactly what I expected. Even though getE() is defined in Base<E>, this INSTANCE of Base<E> is specialized with java.lang.String. While I do not expect reflection on Base<E> itself to tell me the type parameter, I do expect to be able to correlate the actual type parameters and figure this out indirectly.
I dont get your question. You already figured that the type parameter is String. So you know that a method that returns E ... must return String when called on a class that sets String for E. Or am I missing something?
That's the question. I "know" that. But how do I write code that "knows" that too? All I have is a TypeVariable for the return type. How do I know that it is actually java.lang.String instead of E? Anyway, I think I can get there, but it will require some correlation. I'll update teh sample code once I get it figured out.
Still dont get you. You have shown code that finds that E is String. Now you know that any method returnning "E" ... would return String on the derived class.
The code only shows that the specialized return type is "E", and that the first type parameter is "String"; what's missing is the connection that the first type parameter is called "E". This is something we know for this specific example, but cannot generalize.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.