`

[转]java runtime generic type discovery(Reflecting )

阅读更多
Code by Any Other Name
Reflecting generics
by Ian Robertson
June 23, 2007

Summary
Type arguments to generic classes are not available for reflection at runtime - or are they? The type arguments for statically declared types can be discovered at runtime. A look at how to do this, and why you might want to.

--------------------------------------------------------------------------------


Probably the most common complaint about generics in Java is that they are not reified - there is not a way to know at runtime that a List<String> is any different from a List<Long>. I've gotten so used to this that I was quite surprised to run across Neil Gafter's work on Super Type Tokens. It turns out that while the JVM will not track the actual type arguments for instances of a generic class, it does track the actual type arguments for subclasses of generic classes. In other words, while a new ArrayList<String>() is really just a new ArrayList() at runtime, if a class extends ArrayList<String>, then the JVM knows that String is the actual type argument for List's type parameter.

Super type tokens are a nice trick to distinguish between types which have the same raw type, but different type arguments. What they don't easily provide, however, is an easy means of discovering what the type argument for a generic type parameter is. I recently ran across a situation where I wanted to do exactly that while trying to write an abstract base class to take some of the work out of implementing Hibernate's UserType interface:

 
public interface UserType {
    /**
     * The class returned by <tt>nullSafeGet()</tt>.
     */
    public Class returnedClass();

    /**
     * Retrieve an instance of the mapped class from a JDBC resultset.
     */
    public Object nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException;

    /**
     * Write an instance of the mapped class to a prepared statement.
     */
    public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException;

    ...
  } 

UserType is used to provide custom mappings between database values and Java objects. For example, one might need a UserType implementation to map a textual representation of dates (stored as VARCHARs) to the Date class. Because Hibernate needs to support Java 1.4, the UserType interface is not itself generic, but it makes plenty of sense for a base class which implements it to take advantage of generics. In a reified world I might have something like:
 
public abstract class AbstractUserType<T> implements UserType {

    public Class returnedClass() {
      return T.class; //not remotely legal in Java 5 or 6.
    }

    abstract public T nullSafeGet(ResultSet rs, String[] names, Object owner) throws SQLException;

    abstract protected void set(PreparedStatement st, T value, int index) throws SQLException;

    public void nullSafeSet(PreparedStatement st, Object value, int index) throws SQLException {
      set(st, (T) value, index);
    }

  } 
Unfortunately, because of type erasure, the "obvious" implementation of returnedClass() doesn't work. Indeed, an often used pattern in generic programming is to require the type argument class as a method parameter:
 
public abstract class AbstractUserType<T> implements UserType {

    private Class<T> returnedClass;
    protected AbstractUserType(Class<T> returnedClass) {
      this.returnedClass = returnedClass;
    }
    public Class returnedClass() {
      return returnedClass;
    }

    ...
  } 

While this works, it does so at the cost of forcing clients to repeat themselves:
 
public class DateType 
    extends AbstractUserType<Date> { // One for the money
  
    public DateType() {
      super(Date.class); // Two for the show
    }

    ...
  } 

It turns out, however, that even though we cannot access the type of T directly, we can get at our current class, and use the new interfaces extending java.lang.reflect.Type (introduced in Java 5) to get at what we need. A new method on Class was introduced, getGenericSuperclass(). If the class's parent is a generic class, then this will return a ParameterizedType. The getActualTypeArguments() method on ParameterizedType in turn provides an array of the actual type arguments that were used in extending the parent class.
At first glance, then, it seems that the following ought to do the trick:

 
public abstract class AbstractUserType<T> implements UserType {
    ...
    public Class returnedClass {
      ParameterizedType parameterizedType =
        (ParameterizedType) getClass().getGenericSuperClass();
     return (Class) parameterizedtype.getActualTypeArguments()[0];
    }
    ...
  } 

Indeed, for a class that directly extends AbstractUserType (and provides a non-array class for the type parameter), this works well. However, in general, several problems can occur:

For a class extending AbstractUserType<int[]>, the result of the call to getActualTypeArguments()[0] will be a GenericArrayType, even though one might expect it to be of type Class.
If Child extends AbstractUserType, and Grandchild extends Child, then the type returned by Grandchild.class.getGenericSuperClass() will be referencing Child, not AbstractUserType, and hence any actual type arguments would be those provided by Grandchild. Even worse, if Child is not itself a generic class, then Grandchild.class.getGenericSuperClass() will return Child.class, which is of type Class, not ParameterizedType.
Given class declarations:
 public class Child<S> extends AbstractUserType<T>{...}

  public class GrandChild extends Child<Long>{...} 
then Child.class.getGenericSuperClass() will return a ParameterizedType whose actual type argument is a TypeVariable representing the type parameter S. This type variable (or one which is .equals() to it) will also be the sole element of the array returned by Grandchild.class.getTypeParameters(). To get the "actual actual" type argument to AbstractUserType, it is necessary to link these two together.
That said, it is possible to accomplish what we want. The first step is to provide a method which might have been polite for Sun to include in the Type interface itself (as it stands, Type is strictly a marker interface):
 /**
   * Get the underlying class for a type, or null if the type is a variable type.
   * @param type the type
   * @return the underlying class
   */
  public static Class<?> getClass(Type type) {
    if (type instanceof Class) {
      return (Class) type;
    }
    else if (type instanceof ParameterizedType) {
      return getClass(((ParameterizedType) type).getRawType());
    }
    else if (type instanceof GenericArrayType) {
      Type componentType = ((GenericArrayType) type).getGenericComponentType();
      Class<?> componentClass = getClass(componentType);
      if (componentClass != null ) {
        return Array.newInstance(componentClass, 0).getClass();
      }
      else {
        return null;
      }
    }
    else {
      return null;
    }
  } 

Note that we basically "give up" if we hit an unbound type variable; since the goal here is to find a class, we might as well.
The next step is a bit more involved. We need to look at the actual type arguments provided to the super class of the class in question. If that super class is the base class we are interested in, then we are done. Otherwise, we need to repeat this process. However, the actual type arguments we have just looked at may themselves be used as actual type arguments to the next class up the inheritance hierarchy. Unfortunately, Java will not track this for us; we'll need to do it ourselves.

 
/**
   * Get the actual type arguments a child class has used to extend a generic base class.
   *
   * @param baseClass the base class
   * @param childClass the child class
   * @return a list of the raw classes for the actual type arguments.
   */
  public static <T> List<Class<?>> getTypeArguments(
    Class<T> baseClass, Class<? extends T> childClass) {
    Map<Type, Type> resolvedTypes = new HashMap<Type, Type>();
    Type type = childClass;
    // start walking up the inheritance hierarchy until we hit baseClass
    while (! getClass(type).equals(baseClass)) {
      if (type instanceof Class) {
        // there is no useful information for us in raw types, so just keep going.
        type = ((Class) type).getGenericSuperclass();
      }
      else {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        Class<?> rawType = (Class) parameterizedType.getRawType();
  
        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
        TypeVariable<?>[] typeParameters = rawType.getTypeParameters();
        for (int i = 0; i < actualTypeArguments.length; i++) {
          resolvedTypes.put(typeParameters[i], actualTypeArguments[i]);
        }
  
        if (!rawType.equals(baseClass)) {
          type = rawType.getGenericSuperclass();
        }
      }
    }
  
    // finally, for each actual type argument provided to baseClass, determine (if possible)
    // the raw class for that type argument.
    Type[] actualTypeArguments;
    if (type instanceof Class) {
      actualTypeArguments = ((Class) type).getTypeParameters();
    }
    else {
      actualTypeArguments = ((ParameterizedType) type).getActualTypeArguments();
    }
    List<Class<?>> typeArgumentsAsClasses = new ArrayList<Class<?>>();
    // resolve types by chasing down type variables.
    for (Type baseType: actualTypeArguments) {
      while (resolvedTypes.containsKey(baseType)) {
        baseType = resolvedTypes.get(baseType);
      }
      typeArgumentsAsClasses.add(getClass(baseType));
    }
    return typeArgumentsAsClasses;
  } 

Finally, we can accomplish our original goal: 
  public abstract class AbstractUserType<T> implements UserType {
    ...
    public Class returnedClass {
      return getTypeArguments(AbstractUserType.class, getClass()).get(0);
    }
    ...
  } 
While in this case, we are returning raw classes, other use cases might want to see the extended type information for the actual type arguments. Unfortunately, we cannot do this in the case where an actual type argument is a type variable. For example, if the actual type argument for type parameter T is Long, and we are trying to resolve List<T>, we cannot do so without creating a new ParameterizedType instance for the type Long<T>. Since the ParameterizedType implementation provided by Sun is non-instantiable by mere mortals, this would require (re)implementing ParameterizedType. However, since the algorithm for the hashCode method for ParameterizedType is not documented, this cannot be safely accomplished. In particular, it would not be possible to create one of Gafter's Super Type Tokens to represent the actual type argument.

That limitation noted, this can still be useful, and not just for the motivating example above. For example, one could extend ArrayList to get the dynamic return type of toArray() right without help:

 public abstract class TypeAwareArrayList<T> extends ArrayList<T> {
    @Override public T[] toArray() {
      return toArray(
        (T[]) Array.newInstance(
          ReflectionUtils.getTypeArguments(TypeAwareArrayList.class, getClass()).get(0),
        size()));
    }
  } 

Note that TypeAwareArrayList is declared abstract. This forces client code to extend it, if only trivially, so that the type information is available:

 TypeAwareArrayList<String> stringList
    = new TypeAwareArrayList<String>(){}; // notice the trivial anonymous inner class
  ...
  String[] stringArray = stringList.toArray(); 

While the discussion above has been focused on subclasses, similar generics-aware reflection can be done for methods and fields. As with classes, the main thing to remember is that while the dynamic runtime typing of objects is ignorant of generic typing, the type arguments to statically declared types can be discovered through reflection. Hopefully, Java 7 can "erase erasure", and get rid of this frustrating distinction.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics