2

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.

Wheezil
  • 2,611
  • 1
  • 17
  • 32
  • And just for the record: your code works with Java8. I am not sure it would work with older versions of Java. – GhostCat Jul 31 '17 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. – Wheezil Jul 31 '17 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! – GhostCat Jul 31 '17 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? – GhostCat Jul 31 '17 at 17:04

3 Answers3

4

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.

Radiodef
  • 35,285
  • 14
  • 78
  • 114
  • Thanks! I was heading down the path of doing all of this by hand, but Guava looks a lot easier. – Wheezil Jul 31 '17 at 15:42
  • One note: Java 8 and later! – Wheezil Jul 31 '17 at 16:02
  • Ouch. Not sure that Guava can be used in my context. I don't have classes T to specialized TypeToken, because I get the classes through reflection. And this doesn't work: TypeToken tt = new TypeToken(clazz) {}; It fails with "TypeToken isn't parameterized". – Wheezil Jul 31 '17 at 16:25
  • 2
    Ah, its TypeToken.of(clazz) – Wheezil Jul 31 '17 at 17:08
  • 1
    It turns out, I can use this with Java 7 after all. I had to get an older version of Guava. – Wheezil Aug 12 '17 at 14:17
3

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());
              }
            }
          }
        }
      }
    }
  }
}
Wheezil
  • 2,611
  • 1
  • 17
  • 32
  • 1
    Note: Even though my answer illustrates the fundamental issues around getting these types, @Radiodef solution with Guava is far superior IMHO. – Wheezil Aug 12 '17 at 14:16
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)

GhostCat
  • 127,190
  • 21
  • 146
  • 218
  • Sigh. This is exactly what I expected. Even though getE() is defined in Base, this INSTANCE of Base is specialized with java.lang.String. While I do not expect reflection on Base itself to tell me the type parameter, I do expect to be able to correlate the actual type parameters and figure this out indirectly. – Wheezil Jul 31 '17 at 14:45
  • 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? – GhostCat Jul 31 '17 at 14:50
  • 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. – Wheezil Jul 31 '17 at 14:54
  • 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. – GhostCat Jul 31 '17 at 14:56
  • 1
    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. – OhleC Jul 31 '17 at 15:05
  • So you are saying that there is no way to retrieve the list of type parameters respectively the concrete types with names in the order as given in the source code? Beyond that : I am not saying that I have a solution. I am only pointing out why the question "does not work". So I would ask the downvoter to explain himself. – GhostCat Jul 31 '17 at 15:45
  • @ohlec So in case you were the one downvoting - please have another look at my answer. – GhostCat Jul 31 '17 at 16:52
  • Your reworked example does indeed print out the actual type parameters. But that is not the challenge. The challenge is, given a Type handed to you by reflection on a method return type, or method argument, get the correct actual type from that. – Wheezil Jul 31 '17 at 21:26
  • And as explained in the other answer: in the absolute majority of cases, you will not be able to determine that. Because "generics specified via extends" is a rather rare case. Most of the time you do a `new ArrayList` somewhere - and then there is no chance getting details. Type erasure matters and won't go away any time soon. – GhostCat Aug 01 '17 at 06:04