2012年4月19日星期四

Crazy Tricks To Play With TypeToken

Guava 12 will include a TypeToken class, that is designed to be the Class<T> for generic types such as Map<Integer, String>.

A few crazy tricks worth mentioning:

1)
When writing a framework or library, it's sometimes useful to get the java.lang.reflect.Parameterized with the type parameters provided at runtime, similar to Guice's Types.newParameterizedType().

Implementing your own ParameterizedType class has a lot of gotchas to deal with (you'll implement equals(), hashCode(), make sure it inter-operates with the built-in Type). At the call site it's also possible to screw up by passing in type parameters that violate the parameterized type's bounds: newParameterizedType(Enum.class, String).

TypeToken.where() provides a type safe yet flexible API to get at any Type, simple or complex:

static <K, V> TypeToken<Map<K, V>> mapType(
    Class<K> keyType, Class<V> valueType) {
  return new TypeToken<Map<K, V>>() {}
      .where(new TypeParameter<K>() {}, keyType)
      .where(new TypeParameter<V>() {}, valueType);
}

Read this as "Give me type of Map<K, V> where K=keyType and V = valueType". Or, if  you speak Haskell:

Map<K, V>
   where
     K = keyType
     V = valueType;

And this method gives an array type:

static <T> TypeToken<T[]> arrayType(TypeToken<T> componentType) {
  return new TypeToken<T[]>() {}
      .where(new TypeParameter<T>() {}, componentType);
}


We get compiler to work for us to guard against wrong number of type parameters, or type parameters that violate bounds: there is no way to create a TypeToken<Enum<String>>.

2)
Again, if you write crazy reflection frameworks, you may sometimes want to know what a method's return type *really* is.

I mean, List's get(int) method returns Object, and we can tell that by calling List.class.getMethod("get", int.class).getReturnType(). We also know that getGenericReturnType() returns <E>, the type parameter of class List.

But how do we tell that List<String>'s get(int) method returns String, through reflection?

resolveType() is designed for this purpose:

Method getMethod = List.class.getMethod("get", int.class);
new TypeToken<List<String>>() {}.resolveType(getMethod.getGenericReturnType());
    => String

3)
How do you tell if objects returned by method give() can be passed to method take() --- assuming take() accepts one parameter only?

If we disregard generics, it would be as simple (yuch) as:

take.getParameterTypes()[0]
    .isAssignableFrom(give.getReturnType())

But tough, let's consider generics. What if give() and take() are declared as the following?

<K> ImmutableMap<K, Integer> give(K key);
void take(Map<?, ? extends Number&Comparable> m);

JDK provides no API to do this.

TypeToken#isAssignableFrom() works almost like Class#isAssignableFrom(), except it also works for parameterized types, generic array types, wildcard types, and type variables:

TypeToken.of(take.getGenericParameterTypes()[0])
    .isAssignableFrom(give.getGenericReturnType());
  => true

And if instead take() is declared as the following:
void take(Map<Integer, String> m);

The result will be false.

So have at it. Our daily Java life shouldn't be troubled with reflection craziness like these. But they can be helpful in the unfortunate dark hours. :)