A Limitation of Super Type Tokens
Watching Josh Bloch's presentation at JavaOne about new topics in the second edition of Effective Java makes me want to go out and get my own copy. Unfortunately, he's not scheduled to have the new edition in print until later this year.
There was a coincidental adjacency between two slides in Josh's talk that made me think a bit more about the idea of Super Type Tokens. The last slide of his discussion of generics gave a complete implementation of the mind-expanding Typesafe Heterogenous Containers (THC) pattern using Super Type Tokens:
import java.lang.reflect.*;
public abstract class TypeRef<T> {
private final Type type;
protected TypeRef() {
ParameterizedType superclass = (ParameterizedType)
getClass().getGenericSuperclass();
type = superclass.getActualTypeArguments()[0];
}
@Override public boolean equals (Object o) {
return o instanceof TypeRef &&
((TypeRef)o).type.equals(type);
}
@Override public int hashCode() {
return type.hashCode();
}
}
public class Favorites2 {
private Map<TypeRef<?>, Object> favorites =
new HashMap< TypeRef<?> , Object>();
public <T> void setFavorite(TypeRef<T> type, T thing) {
favorites.put(type, thing);
}
@SuppressWarning("unchecked")
public <T> T getFavorite(TypeRef<T> type) {
return (T) favorites.get(type);
}
public static void main(String[] args) {
Favorites2 f = new Favorites2();
List<String> stooges = Arrays.asList(
"Larry", "Moe", "Curly");
f.setFavorite(new TypeRef<List<String>>(){}, stooges);
List<String> ls = f.getFavorite(
new TypeRef<List<String>>(){});
}
}
But on the very next slide, the very first bullet of the summary of his presentation reminds us
- Don't ignore compiler warnings.
This was referring to Josh's advice earlier in the presentation not to ignore or suppress unchecked compiler warnings without trying to understand them. Ideally, you should only suppress these warnings when you have good reason to believe that the code is type-safe, even though you might not be able to convince the compiler of that fact.
The method Favorites2.getFavorite
,
above, is annotated
to suppress a warning from the compiler. Without that annotation, the
compiler complains about the cast to the type T, a type parameter. Is
this code demonstrably type safe? Is it possible to cause this cast to
fail using code that is otherwise completely type safe? Unfortunately,
the cast is not safe:
class Oops { static Favorites2 f = new Favorites2(); static <T> List<T> favoriteList() { TypeRef<List<T>> ref = new TypeRef<List<T>>(){}; List<T> result = f.getFavorite(ref); if (result == null) { result = new ArrayList<T>(); f.setFavorite(ref, result); } return result; } public static void main(String[] args) { List<String> ls = favoriteList(); List<Integer> li = favoriteList(); li.add(1); for (String s : ls) System.out.println(s); } }
This program compiles without warning, but it exposes the
loopole in the type system created by the cast to T in Favorites2.getFavorite
.
The compiler's warning does, after all, tell us about a weakness in the
type safety of the program.
The issue is a subtle one: TypeRef
treats two types as the same when the underlying java.lang.reflect.Type
objects are equal. A given java.lang.reflect.Type
object represents a particular static
type appearing in the source, but if it is a type variable it can
represent a different dynamic
type from
one point in the program's execution to another. The program Oops
exploits that mismatch.
The Super Type Token pattern can be redeemed by disallowing the use of type variables anywhere in the Type object it stores. That can be enforced at runtime (but not at compile time) in the constructor.
Perhaps a better solution would be to reify generics (i.e., "erase erasure") in the language, making all this nonsense unnecessary.