Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I generate an accurate generics expression for a Java class field?

I am trying to reason about generics at runtime. There are several great libraries to do this (e.g., gentyref, ClassMate and Guava). However, their usage is a little over my head.

Specifically, I want to extract an expression which matches a particular field in the context of a subclass.

Here is an example using gentyref:

import com.googlecode.gentyref.GenericTypeReflector;

import java.lang.reflect.Field;
import java.lang.reflect.Type;

public class ExtractArguments {

  public static class Thing<T> {
    public T thing;
  }

  public static class NumberThing<N extends Number> extends Thing<N> { }

  public static class IntegerThing extends NumberThing<Integer> { }

  public static void main(final String... args) throws Exception {
    final Field thing = Thing.class.getField("thing");

    // naive type without context
    Class<?> thingClass = thing.getType(); // Object
    System.out.println("thing class = " + thingClass);
    Type thingType = thing.getGenericType(); // T
    System.out.println("thing type = " + thingType);
    System.out.println();

    // exact types without adding wildcard
    Type exactThingType = GenericTypeReflector.getExactFieldType(thing, Thing.class);
    System.out.println("exact thing type = " + exactThingType);
    Type exactNumberType = GenericTypeReflector.getExactFieldType(thing, NumberThing.class);
    System.out.println("exact number type = " + exactNumberType);
    Type exactIntegerType = GenericTypeReflector.getExactFieldType(thing, IntegerThing.class);
    System.out.println("exact integer type = " + exactIntegerType);
    System.out.println();

    // exact type with wildcard
    final Type wildThingType = GenericTypeReflector.addWildcardParameters(Thing.class);
    final Type betterThingType = GenericTypeReflector.getExactFieldType(thing, wildThingType);
    System.out.println("better thing type = " + betterThingType);
    final Type wildNumberType = GenericTypeReflector.addWildcardParameters(NumberThing.class);
    final Type betterNumberType = GenericTypeReflector.getExactFieldType(thing, wildNumberType);
    System.out.println("better number type = " + betterNumberType);
    final Type wildIntegerType = GenericTypeReflector.addWildcardParameters(IntegerThing.class);
    final Type betterIntegerType = GenericTypeReflector.getExactFieldType(thing, wildIntegerType);
    System.out.println("better integer type = " + betterIntegerType);
    System.out.println();

    System.out.println("desired thing type = T");
    System.out.println("desired number thing type = N extends Number");
    System.out.println("desired integer thing type = Integer");
  }

}

And here is the output:

thing class = class java.lang.Object
thing type = T

exact thing type = class java.lang.Object
exact number type = class java.lang.Object
exact integer type = class java.lang.Integer

better thing type = capture of ?
better number type = capture of ?
better integer type = class java.lang.Integer

desired thing type = T
desired number thing type = N extends Number
desired integer thing type = Integer

I know the betterThingType Type object (a gentyref-specific implementation) is more sophisticated than what is shown by toString() here. But I am guessing I need to invoke getExactFieldType again with a non-wildcard Type to get what I'm looking for.

My main requirement is that I need an expression which could become part of a code-generated source file that could be successfully compiled—or at least compiled with minimal modification. I am open to using whatever library is best for the job.

like image 801
ctrueden Avatar asked Jan 25 '15 23:01

ctrueden


People also ask

How do you make a class generic in Java?

A Generic Version of the Box Class To update the Box class to use generics, you create a generic type declaration by changing the code "public class Box" to "public class Box<T>". This introduces the type variable, T, that can be used anywhere inside the class.

How do you declare a generic type in a class explain?

The declaration of a generic class is almost the same as that of a non-generic class except the class name is followed by a type parameter section. The type parameter section of a generic class can have one or more type parameters separated by commas.

Which one of the following construct is used to create generic classes or methods?

We use <T> to create a generic class, interface, and method. The T is replaced with the actual type when we use it.

How do you define a generic object in Java?

Generics Work Only with Reference Types: When we declare an instance of a generic type, the type argument passed to the type parameter must be a reference type. We cannot use primitive data types like int, char. Test<int> obj = new Test<int>(20);


1 Answers

To get this kind of information, you must determine whether or not an actual type (e.g Integer) has been supplied to the generic type parameter. If not, you will need to get the type parameter name, as it's known in the class you need, along with any bounds.

This turns out to be quite complicated. But first, let's go over some of the reflection techniques and methods we'll use in the solution.

First, Field's getGenericType() method returns the Type information needed. Here, the Type can be a simple Class if an actual class is supplied as the type, e.g. Integer thing;, or it can be a TypeVariable, representing a generic type parameter as you have defined it in Thing, e.g. T thing;.

If it's a generic type, then we will need to know the following:

  • In what class this type was originally declared. This is retrieved with Field's getDeclaringClass method.
  • In each subclass, from the original class that declared the Field, what type arguments were supplied in the extends clause. These type arguments may themselves be actual types like Integer, or they may be their own class's generic type parameters. Complicating matters, these type parameters may be differently named, and they could be declared in a different order than in the superclass. The extends clause data can be retrieved by calling Class's getGenericSuperclass() method, which returns a Type that can be a simple Class, e.g Object, or it could be a ParameterizedType, e.g. Thing<N> or NumberThing<Integer>.
  • A class's own type parameters can be retrieved with Class's getTypeParameters() method, which returns an array of TypeVariables.
  • From a TypeVariable you can extract the name, e.g. T, and the bounds, as an array of Type objects, e.g. Number for N extends Number.

For the generic type parameter, we need to track which subclass type arguments match the original generic type parameter, down through the class hierarchy, until we either reach the original Class, in which we report the generic type parameter with any bounds, or we reach an actual Class object, in which we report the class.

Here is a program, based on your classes, that reports your desired information.

It must create a Stack of Classes, going from the original class up to the class that declares the field. Then it pops the classes, walking down the class hierarchy. It finds the type argument in the current class that matches the type parameter from the previous class, noting down any type parameter name changes and new positions of the new type argument provided by the current class. E.g. T becomes N extends Number when going from Thing to NumberThing. The loop iterations stop when the type argument is an actual class, e.g. Integer, or if we've reached the original class, in which case we report the type parameter name and any bounds, e.g. N extends Number.

I have also included a couple additional classes, Superclass and Subclass, where Subclass reverses the order of generic type arguments that are declared in Superclass, to provide additional testing. I also included SpecificIntegerThing (non-generic), as a test case so that the iteration stops at IntegerThing, to report Integer, before it reaches SpecificIntegerThing in the stack.

// Just to have some bounds to report.
import java.io.Serializable;
import java.util.RandomAccess;

// Needed for the implementation.
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Stack;

public class ExtractArguments {

   public static class Thing<T> {
      public T   thing;
   }

   public static class NumberThing<N extends Number> extends Thing<N> {}

   public static class IntegerThing extends NumberThing<Integer> {}

   public static class SpecificIntegerThing extends IntegerThing {}

   public static class Superclass<A extends Serializable, B> {
      public A thing;
   }

   // A and B are reversed in the extends clause!
   public static class Subclass<A, B extends RandomAccess & Serializable>
      extends Superclass<B, A> {}  

   public static void main(String[] args)
   {
      for (Class<?> clazz : Arrays.asList(
              Thing.class, NumberThing.class,
              IntegerThing.class, SpecificIntegerThing.class,
              Superclass.class, Subclass.class))
      {
         try
         {
            Field field = clazz.getField("thing");
            System.out.println("Field " + field.getName() + " of class " + clazz.getName() + " is: " +
                    getFieldTypeInformation(clazz, field));
         }
         catch (NoSuchFieldException e)
         {
            System.out.println("Field \"thing\" is not found in class " + clazz.getName() + "!");
         }
      }
   }

The getFieldTypeInformation method does the work with the stack.

   private static String getFieldTypeInformation(Class<?> clazz, Field field)
   {
      Type genericType = field.getGenericType();
      // Declared as actual type name...
      if (genericType instanceof Class)
      {
         Class<?> genericTypeClass = (Class<?>) genericType;
         return genericTypeClass.getName();
      }
      // .. or as a generic type?
      else if (genericType instanceof TypeVariable)
      {
         TypeVariable<?> typeVariable = (TypeVariable<?>) genericType;
         Class<?> declaringClass = field.getDeclaringClass();
         //System.out.println(declaringClass.getName() + "." + typeVariable.getName());

         // Create a Stack of classes going from clazz up to, but not including, the declaring class.
         Stack<Class<?>> stack = new Stack<Class<?>>();
         Class<?> currClass = clazz;
         while (!currClass.equals(declaringClass))
         {
            stack.push(currClass);
            currClass = currClass.getSuperclass();
         }
         // Get the original type parameter from the declaring class.
         int typeVariableIndex = -1;
         String typeVariableName = typeVariable.getName();
         TypeVariable<?>[] currTypeParameters = currClass.getTypeParameters();
         for (int i = 0; i < currTypeParameters.length; i++)
         {
            TypeVariable<?> currTypeVariable = currTypeParameters[i];
            if (currTypeVariable.getName().equals(typeVariableName))
            {
               typeVariableIndex = i;
               break;
            }
         }

         if (typeVariableIndex == -1)
         {
            throw new RuntimeException("Expected Type variable \"" + typeVariable.getName() +
                    "\" in class " + clazz + "; but it was not found.");
         }

         // If the type parameter is from the same class, don't bother walking down
         // a non-existent hierarchy.
         if (declaringClass.equals(clazz))
         {
            return getTypeVariableString(typeVariable);
         }

         // Pop them in order, keeping track of which index is the type variable.
         while (!stack.isEmpty())
         {
            currClass = stack.pop();
            // Must be ParameterizedType, not Class, because type arguments must be
            // supplied to the generic superclass.
            ParameterizedType superclassParameterizedType = (ParameterizedType) currClass.getGenericSuperclass();
            Type currType = superclassParameterizedType.getActualTypeArguments()[typeVariableIndex];
            if (currType instanceof Class)
            {
               // Type argument is an actual Class, e.g. "extends ArrayList<Integer>".
               currClass = (Class) currType;
               return currClass.getName();
            }
            else if (currType instanceof TypeVariable)
            {
               TypeVariable<?> currTypeVariable = (TypeVariable<?>) currType;
               typeVariableName = currTypeVariable.getName();
               // Reached passed-in class (bottom of hierarchy)?  Report it.
               if (currClass.equals(clazz))
               {
                  return getTypeVariableString(currTypeVariable);
               }
               // Not at bottom?  Find the type parameter to set up for next loop.
               else
               {
                  typeVariableIndex = -1;
                  currTypeParameters = currClass.getTypeParameters();
                  for (int i = 0; i < currTypeParameters.length; i++)
                  {
                     currTypeVariable = currTypeParameters[i];
                     if (currTypeVariable.getName().equals(typeVariableName))
                     {
                        typeVariableIndex = i;
                        break;
                     }
                  }

                  if (typeVariableIndex == -1)
                  {
                     // Shouldn't get here.
                     throw new RuntimeException("Expected Type variable \"" + typeVariable.getName() +
                         "\" in class " + currClass.getName() + "; but it was not found.");
                  }
               }
            }
         }
      }
      // Shouldn't get here.
      throw new RuntimeException("Missed the original class somehow!");
   }

The getTypeVariableString method helps to generate the type parameter name and any bounds.

   // Helper method to print a generic type parameter and its bounds.
   private static String getTypeVariableString(TypeVariable<?> typeVariable)
   {
      StringBuilder buf = new StringBuilder();
      buf.append(typeVariable.getName());
      Type[] bounds = typeVariable.getBounds();
      boolean first = true;
      // Don't report explicit "extends Object"
      if (bounds.length == 1 && bounds[0].equals(Object.class))
      {
         return buf.toString();
      }
      for (Type bound : bounds)
      {
         if (first)
         {
            buf.append(" extends ");
            first = false;
         }
         else
         {
            buf.append(" & ");
         }
         if (bound instanceof Class)
         {
            Class<?> boundClass = (Class) bound;
            buf.append(boundClass.getName());
         }
         else if (bound instanceof TypeVariable)
         {
            TypeVariable<?> typeVariableBound = (TypeVariable<?>) bound;
            buf.append(typeVariableBound.getName());
         }
      }
      return buf.toString();
   }
}

This the output:

Field thing of class ExtractArguments$Thing is: T
Field thing of class ExtractArguments$NumberThing is: N extends java.lang.Number
Field thing of class ExtractArguments$IntegerThing is: java.lang.Integer
Field thing of class ExtractArguments$SpecificIntegerThing is: java.lang.Integer
Field thing of class ExtractArguments$Superclass is: A extends java.io.Serializable
Field thing of class ExtractArguments$Subclass is: B extends java.util.RandomAccess & java.io.Serializable
like image 116
rgettman Avatar answered Sep 26 '22 19:09

rgettman