Reputation: 11483
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
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
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