![]() |
Sponsored Link •
|
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.
Advertisement
|
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 VARCHAR
s) 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);
}
}
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;
}
...
}
public class DateType
extends AbstractUserType<Date> { // One for the money
public DateType() {
super(Date.class); // Two for the show
}
...
}
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:
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
.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
.
public class Child<S> extends AbstractUserType<T>{...}
public class GrandChild extends Child<Long>{...}
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.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;
}
}
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;
}
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.
Have an opinion? Readers have already posted 26 comments about this weblog entry. Why not add yours?
If you'd like to be notified whenever Ian Robertson adds a new entry to his weblog, subscribe to his RSS feed.
![]() | Ian Robertson is an application architect at Verisk Health. He is interested in finding concise means of expression in Java without sacrificing type safety. He contributes to various open source projects, including jamon and pojomatic. |
Sponsored Links
|