Core Java

Advanced Java generics: retreiving generic type arguments

After their introduction in the JDK5, Java generics quickly became an integral element of many Java programs. However, as easy Java generics seem at first glance, as quickly a programer can get lost with this feature.

Most Java programers are aware of the Java compiler’s type erasure. Generally speaking, type erasure means that all generic type information about a Java class is lost during the compilation of its source code. This is a tribute to Java’s backwards compatibility: all generic variations of a Java class share a single representation within a running Java application. If an instance of ArrayList<String> would have to remember that its generic type was of type String, it would have to store this information somewhere within its functional description in order to indicate that for example List.get actually returns a String type. (By functional description I refer to properties which are shared among all instances of a class. This includes for example method or field definitions. In contrast to its functional description, an instance’s state which is individual to each instance is stored in its object representation.) The functional description of the ArrayList<String> instance is thus represented by its class ArrayList.class. Since the ArrayList.class instance is however shared with other instances which could also be of type ArrayList<Integer>, this would already require to have two different versions of ArrayList.class. Such modifications of the class representation would however be incomprehensible to older JREs and thus break the backwards compatibility of Java applications. As a consequence, the following comparison will always succeed:

assert new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass();

Since such a comparison is conducted at run time where the generic type of a class was already erased, this comparison translates to ArrayList.class == ArrayList.class what is trivial. Or more specifically, the running application will determine that ArrayList.class is equal to itself and return true, despite of String.class != Integer.class. This is a major difference of Java to other programming languages like for example C++ and also the reason for a common complaint about Java. (Academically speaking, C++ does not actually know generic types. Instead, C++ offers templates which are however similar to generics.)

So far, this is nothing new to many developers. However, contrary to popular belief it is sometimes possible to retrieve generic type information even during run time. Before explaining, when this is possible, let us look at an example. For this we define the following two classes:

class MyGenericClass<T> { }
class MyStringSubClass extends MyGenericClass<String> { }

MyGenericClass has a single argument for a generic type T. MyStringSubClass extends this generic class and is assigning T = String as its type parameter. As a result, the Java compiler is able to store the information about the generic argument’s type String of superclass MyGenericClass in the byte code of its subclass MyStringSubClass. This modification can be achieved without breaking backwards compatibility, because this information is simply stored in a region of the compiled class’s byte code which is ignored by old JRE versions. At the same time, all instances of MyStringSubClass can still share a single class representation, since T = String is set for all instances of MyStringSubClass.

But how can we get hold of this information stored in the byte code? The Java API provides the Class.getGenericSuperclass method which can be used to receive an instance of type Type. If the direct superclass is in fact generic, the returned instance is additionally of type ParameterizedType and can be cast to it. (Type is nothing but a marker interface. The actual instance will be an instance of the internal ParameterizedTypeImpl class, you should however always cast to the interface.) Thanks to a cast to the ParameterizedType interface, you can now call the method ParameterizedType.getActualTypeArguments to retrieve an array which is again of type Type. Any generic type argument of the generic superclass will be contained in this array at the same index as in the type definition. Any Type instance which represents a non-generic class is simply an implementation of a Java Class class. (Assuming, you are not handeling an array where the returned type is of GenericArrayType. I will skip this scenario in this article for the sake of simplicity.)

Now we can make use of this knowledge to write a utility function:

public static Class<?> findSuperClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
  Class<?> subClass = instance.getClass();
  while (subClass != subClass.getSuperclass()) {
    // instance.getClass() is no subclass of classOfInterest or instance is a direct instance of classOfInterest
    subClass = subClass.getSuperclass();
    if (subClass == null) throw new IllegalArgumentException();
  }
  ParameterizedType parameterizedType = (ParameterizedType) subClass.getGenericSuperclass();
  return (Class<?>) parameterizedType.getActualTypeArguments()[parameterIndex];
}

This function will browse through the class hierarchy of instance until it recognizes classOfInterest to be the next direct sub class in the hierarchy. When this is the case, this super class will be retrieved by using the Class.getGenericSuperclass method. As described above, this method returns a class’s super class in a wrapped representation (ParamererizedType) which includes the generic types which are found in the subclass. This allows us to successfully run the following application:

Class<?> genericType = findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);
assert genericType == String.class;

Be however aware that

findSuperClassParamerterType(new MyGenericClass<String>(), MyGenericClass.class, 0)

will throw an exception in this implementation. As stated before: the generic information can only be retrieved with the help of a subclass. MyGenericClass<String> is however not a subclass of MyGenericClass.class but a direct instance with a generic argument. But without an explicit subclass, there is no <something>.class representation to store the String argument. Therefore this time, the generic type was irretrievably erased during compilation. For this reason, it is a good practice to define MyGenericClass to be abstract, if you are planing on performing such queries on a class.

However, we have not yet solved the problem, since there are several pitfalls we ignored so far. To show why, think of the following class hierarchy:

class MyGenericClass<T> { }
class MyGenericSubClass<U> extends MyGenericClass<U>
class MyStringSubSubClass extends MyGenericSubClass<String> { }

If we now call

findSuperClassParameterType(new MyStringSubClass(), MyGenericClass.class, 0);

an exception will be thrown. But why is this so? So far, we assumed that the type parameter T for MyGenericClass was stored in a direct subclass. In our first example, this was MyStringSubClass which mapped the generic parameter T = String. In contrast, now MyStringSubSubClass stores a reference U = String while MyGenericSubClass only knows that U = T. U is however not an actual class but a type variable of Java type TypeVariable. If we want to resolve this hierarchy, we have to resolve all of these dependencies. This can be achieved by adjusting our example code:

public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
  Map<Type, Type> typeMap = new HashMap<Type, Type>();
  Class<?> instanceClass = instance.getClass();
  while (classOfInterest != instanceClass.getSuperclass()) {
    extractTypeArguments(typeMap, instanceClass);
    instanceClass = instanceClass.getSuperclass();
    if (instanceClass == null) throw new IllegalArgumentException();
  }
 
  ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass();
  Type actualType = parameterizedType.getActualTypeArguments()[parameterIndex];
  if (typeMap.containsKey(actualType)) {
    actualType = typeMap.get(actualType);
  }
  if (actualType instanceof Class) {
    return (Class<?>) actualType;
  } else {
    throw new IllegalArgumentException();
  }
 
private static void extractTypeArguments(Map<Type, Type> typeMap, Class<?> clazz) {
  Type genericSuperclass = clazz.getGenericSuperclass();
  if (!(genericSuperclass instanceof ParameterizedType)) {
    return;
  }
 
  ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
  Type[] typeParameter = ((Class<?>) parameterizedType.getRawType()).getTypeParameters();
  Type[] actualTypeArgument = parameterizedType.getActualTypeArguments();
  for (int i = 0; i < typeParameter.length; i++) {
    if(typeMap.containsKey(actualTypeArgument[i])) {
      actualTypeArgument[i] = typeMap.get(actualTypeArgument[i]);
    }
    typeMap.put(typeParameter[i], actualTypeArgument[i]);
  }
}

The above code will resolve any chained generic type definitions by tracking them in a map. Please note that it is not enough to examine all type definitions by a specific index since MyClass<A,B> extends MyOtherClass<B,A> defines a perfectly legal subtype.

However, we are still not done. Again, we will look at an example first:

class MyGenericOuterClass<U> {
  public class MyGenericInnerClass<U> { }
}
class MyStringOuterSubClass extends MyGenericOuterClass<String> { }
 
MyStringOuterSubClass.MyGenericInnerClass inner = new MyStringOuterSubClass().new MyGenericInnerClass();

This time a reflection on the inner class by calling

findSuperClassParameterType(inner, MyGenericInnerClass.class, 0);

will fail. At first glance, this might seem consequent. We are looking for the generic argument type in MyGenericInnerClass on an instance of the same class. As we described above, this is usually not possible since no generic type information can be stored in MyGenericInnerClass.class. Here however, we examine an instance of a (non-static) inner class of a generic class’s subtype. MyStringOuterSubClass knows that U = String. We have to take this into account when reflecting on the parameter type of MyGenericInnterClass.

Now here is where things get really tricky. In order to find generic declarations in outer classes, we have to first get hold of this outer class. This can be achieved by reflection and the fact that the Java compiler adds a synthetic (this means without source code representation) field this$0 to any inner class. This field can be retrieved by calling Class.getDeclaredField(“this$0”). By obtaining the instance of the outer class in which the current inner class is contained, we automatically gain access to its Java class. Now we could just proceed as above and scan the enclosing class for generic definitions and add them to out map. However, type variable representation of U in MyGenericOuterClass will not equal the representation of U in MyGenericInnerClass. For all we know, MyGenericInnerClass could be static and define its own generic variable name space. Therefore, any TypeVariable type which represent generic variables in the Java API, is equipped with a genericDeclaration property. If two generic variables were defined in different classes, the TypeVariable representations are not equal by their definition, even if they share a name in the same name space by one class being a non-static inner class of the other.

Therefore we have to do the following:

  1. First, try to find a generic type in the inner classes super class hierarchy. Just as you would do with a non-nested class.
  2. If you cannot resolve the type: For the (non-static) inner class and all of its outer classes, resolve the type variables as complete as possible. This can be achieved by the same extractTypeArguments algorithm and is basically 1. for each nested class. We can get hold of the outer classes by checking if the this$0 field is defined for an inner class.
  3. Check if one of the outer classes contains a definition for a generic variable with an identical variable name. If this is the case, you found the actual type of a generic variable you were looking for.

In code, this looks like this:

public static Class<?> findSubClassParameterType(Object instance, Class<?> classOfInterest, int parameterIndex) {
  Map<Type, Type> typeMap = new HashMap<Type, Type>();
  Class<?> instanceClass = instance.getClass();
  while (classOfInterest != instanceClass.getSuperclass()) {
    extractTypeArguments(typeMap, instanceClass);
    instanceClass = instanceClass.getSuperclass();
    if (instanceClass == null) throw new IllegalArgumentException();
  }
 
  ParameterizedType parameterizedType = (ParameterizedType) instanceClass.getGenericSuperclass();
  Type actualType = parameterizedType.getActualTypeArguments()[parameterIndex];
  if (typeMap.containsKey(actualType)) {
    actualType = typeMap.get(actualType);
  }
 
  if (actualType instanceof Class) {
    return (Class<?>) actualType;
  } else if (actualType instanceof TypeVariable) {
    return browseNestedTypes(instance, (TypeVariable<?>) actualType);
  } else {
    throw new IllegalArgumentException();
  }
}
 
private static Class<?> browseNestedTypes(Object instance, TypeVariable<?> actualType) {
  Class<?> instanceClass = instance.getClass();
  List<Class<?>> nestedOuterTypes = new LinkedList<Class<?>>();
  for (
    Class<?> enclosingClass = instanceClass.getEnclosingClass();
    enclosingClass != null;
    enclosingClass = enclosingClass.getEnclosingClass()) {
    try {
      Field this$0 = instanceClass.getDeclaredField("this$0");
      Object outerInstance = this$0.get(instance);
      Class<?> outerClass = outerInstance.getClass();
      nestedOuterTypes.add(outerClass);
      Map<Type, Type> outerTypeMap = new HashMap<Type, Type>();
      extractTypeArguments(outerTypeMap, outerClass);
      for (Map.Entry<Type, Type> entry : outerTypeMap.entrySet()) {
        if (!(entry.getKey() instanceof TypeVariable)) {
          continue;
        }
        TypeVariable<?> foundType = (TypeVariable<?>) entry.getKey();
        if (foundType.getName().equals(actualType.getName())
            && isInnerClass(foundType.getGenericDeclaration(), actualType.getGenericDeclaration())) {
          if (entry.getValue() instanceof Class) {
            return (Class<?>) entry.getValue();
          }
          actualType = (TypeVariable<?>) entry.getValue();
        }
      }
    } catch (NoSuchFieldException e) { /* this should never happen */ } catch (IllegalAccessException e) { /* this might happen */}
 
  }
  throw new IllegalArgumentException();
}
 
private static boolean isInnerClass(GenericDeclaration outerDeclaration, GenericDeclaration innerDeclaration) {
  if (!(outerDeclaration instanceof Class) || !(innerDeclaration instanceof Class)) {
    throw new IllegalArgumentException();
  }
  Class<?> outerClass = (Class<?>) outerDeclaration;
  Class<?> innerClass = (Class<?>) innerDeclaration;
  while ((innerClass = innerClass.getEnclosingClass()) != null) {
    if (innerClass == outerClass) {
      return true;
    }
  }
  return false;
}

Wow, this is ugly! But the above code makes findSubClassParameterType work even with nested classes. We could go into even greater detail, since we can also find types of generic interfaces, generic methods, fields or arrays. The idea of all such extractions however remains the same. If a subclass knows the generic arguments of its super class, they can be retreived via reflections. Otherwise, due to type erasure, the generic arguments will be irretrievably lost at run time.

But in the end, what is this good for? To many developers, this conveys the impression of performed black magic such that they rather avoid writing such code. Admittedly, there are in general easier ways to perform such a query. We could have defined the MyGenericSubclass like this instead:

class MyGenericClass<T> {
  private final Class<T> clazz;
  public MyGenericClass(Class<T> clazz) {
    this.clazz = clazz;
  }
  public Class<T> getGenericClass() {
    return clazz;
  }
}

Of course, this works as well and is even less code. However, when you are writing APIs that are to be used by other developers, you often want them to be as slim and easy as possible. (This can go from writing a big framework to writing software in a team of two.) By the above implementation, you force the users of your class to provide redundant information that you could have retrieved differently. Also, this approach does not work likely well for interfaces where you implicitly require the implementing classes to add corresponding constructors. This matter will become even more relevant when looking towards Java 8 and its functional interfaces (also known as closures or lambda expressions). If you require your generic interfaces to supply a getGenericClass method besides their functional method, you cannot longer use them within a lambda expression.

PS: I hacked this code while I was writing this blog article and never really tested it but by dupa debugging. If you need such functionality, there is an excellent library called gentyref which provides the above analysis and much more.
 

Reference: Advanced Java generics: retreiving generic type arguments from our JCG partner Rafael Winterhalter at the My daily Java blog.

Rafael Winterhalter

Rafael is a software engineer based in Oslo. He is a Java enthusiast with particular interests in byte code engineering, functional programming, multi-threaded applications and the Scala language.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Jörgen
Jörgen
10 years ago

Thanks for the post. Really interesting!

In the first version of the findSuperClassParameterType implementation the while condition looks a little incorrect: subClass != subClass.getSuperclass().

Rafael Winterhalter
10 years ago

Thank you for your feedback. You are of course right, it should of course be
classOfInterest != subClass.getSuperclass()
The intent is to iterate through the class hierarchy until the direct sub class of the “class of interest” is found. With this subclass, the generic version of this class’s super class can be read which represents this “class of interest”.

shahram
shahram
7 years ago

Your solution is nice and useful. However, it does not cover classes that implement generics instead of extending.

Bojan
7 years ago

Possibly the best article I’ve ever read. Thanks!

Laurent
Laurent
4 years ago

This is awesome ! I had started to write the same thing but was only half way through it. That saved me some time :-)

FiruzzZ
FiruzzZ
4 years ago

very useful post.. thanks

Back to top button