Rogue
Rogue

Reputation: 11483

Specifying generics for a class type

I have a method which essentially handles casting for config types, however upon specifying a generic type (such as List), it becomes a problem of how to handle the specific type. In an ideal world, something such as using a type witness:

List<String> someVal = MyConfig.SOME_VAL.<List<String>>.as(List.class);'

(The full as code):

/**
 * Attempts to return the {@link Config} value as a casted type. If the
 * value cannot be casted it will attempt to return the default value. If
 * the default value is inappropriate for the class, the method will
 * throw a {@link ClassCastException}.
 * 
 * @since 0.1.0
 * @version 0.1.0
 * 
 * @param <T> The type of the casting class
 * @param c The class type to cast to
 * @return A casted value, or {@code null} if unable to cast. If the passed
 *         class parameter is of a primitive type or autoboxed primitive,
 *         then a casted value of -1 is returned, or {@code false} for
 *         booleans. If the passed class parameter is for {@link String},
 *         then {@link Object#toString()} is called on the value instead
 */
default public <T> T as(Class<T> c) {
    Validate.notNull(c, "Cannot cast to null");
    Validate.isTrue(Primitives.unwrap(c) != void.class, "Cannot cast to a void type");
    Object o = this.get();
    if (o == null) {
        T back = Reflections.defaultPrimitiveValue(c);
        if (back != null) { //catch for non-primitive classes
            return back;
        }
    }
    if (c == String.class) {
        return (T) String.valueOf(o);
    }
    if (c.isInstance(o)) {
        return c.cast(o);
    }
    if (c.isInstance(this.getDefault())) {
        return c.cast(this.getDefault());
    }
    throw new ClassCastException("Unable to cast config value");
}

So essentially that leaves me with a two-part question: Why can't type witnesses be used for generics on a class (such as List(raw) -> List<String>), and how can I go about supporting retrieving a class with generic bounding without doing extraneous casting? The first point particularly baffles me, since this is perfectly legal:

List<String> test = new ArrayList<>();
test = MyConfig.FRIENDLY_MOBS.as(test.getClass());

Despite it returning a raw-typed list

Upvotes: 2

Views: 287

Answers (2)

Philipp Wendler
Philipp Wendler

Reputation: 11423

Your idea of type witnesses is indeed the way to go, but you need a better type witness that does not only capture the raw type (here List) but also its generic parameters. This is not easy in Java because in most places the generic parameters are not available at runtime due to type erasure. Java's reflection API uses interfaces that are subinterfaces of Type as a runtime representation of generic types, but these are unsuited for your purpose because they do not provide any compile-time type information.

However, with a trick it is possible to achieve what you want. The trick is based on the fact that if a class (example: MyClass) inherits from a generic type (example: List<String>), there is no type erasure. You can at runtime retrieve the information that MyClass inherits from List<String> (using the method Class.getGenericSuperclass()).

Subclassing the actual type we want to pass would however be very inflexible (for example this would not work for final classes). Thus we create a special class (often called TypeToken) from which we can inherit. The TypeToken class has a generic parameter, and in the subclass we specify the type we want to pass as this parameter. Of course creating a special class for every different value you want to pass would usually be quite cumbersome, but fortunately we can use anonymous classes to make this easy.

Putting it all together, a solution could look like the following.

Definition of our type token:

public abstract class TypeToken<T> {}

Definition of method as:

public <T> T as(TypeToken<T> typeToken) {
  Type targetType = typeToken.getClass().getGenericSuperclass();
  // use targetType here, e.g.
  if (targetType instanceof ParameterizedType) { ... }

And calling it:

 List<Integer> list = MyConfig.SOME_VAL.as(new TypeToken<List<String>>() {});

Notice the {} that declares the anonymous subclass and avoids the type erasure.

Even better would be to use an existing class as the type token, for example the class TypeToken of the great Guava library (if you do not know this library yet, also look at what else it offers and consider using it!). This class also provides additional helper methods that make it easer to use the token in the as method (directly using Type instances can be difficult). The Guava wiki has more information on its TypeToken class.

If you are worried about creating too many classes, you can of course easily provide a few default instances for common cases like TypeToken<List<String>> etc. Guava's TypeToken also has an of(Class<T>) method that can be used for non-generic types, so the subclasses would be restricted to cases where it is actually necesary.

Other projects also use this trick, for example Guice with class TypeLiteral (explanation), Gson (TypeToken), and Jackson (TypeReference). So I would not worry too much about the amount of subclasses, given that they do not clutter your source code.

Upvotes: 1

uberwach
uberwach

Reputation: 1109

That line is really evil (type erasure / raw type) as there is no check whatsoever whether the Collection type really contains strings.

test = MyConfig.FRIENDLY_MOBS.as(test.getClass());

I think the easiest solution is to write an as method that takes the class object of both the collection type as well as the element class. See this following example (in static scope, so you have to adjust it):

static List<String> words = Arrays.asList("Hello", "Bonjour", "engine");

static public <E, Coll extends Collection<E>> Coll as(Class<? extends Collection> collClass, Class<E> elemClass) {
    if (collClass.isInstance(words)) {
        Collection coll = collClass.cast(words);
        for (Object o : coll)
            if (!elemClass.isInstance(o))
                throw new ClassCastException("BAM");
        return (Coll)coll;
    }

    return null;
}

Now the following behaviour is found:

final List<String> list = as(List.class, String.class); // works
final List<String> list = as(List.class, Integer.class); // compile error: type check
final List<Integer> list = as(List.class, String.class); // compile error: type check
final List<Integer> list = as(List.class, Integer.class); // ClassCastException

As for other attempts: iirc Jackson had some magic TypeToken stuff going on that allowed to capture types such as List<String>. It somehow abused Enum<T> I think...

Upvotes: 1

Related Questions