Dynamic type check matching type parameters

Viewed 103

I wish to implement a method with the following signature:

<T> void function(Class<T> clazz, Consumer<T> consumer)

This function takes an Object obj (supplied elsewhere), uses clazz.cast(obj) to cast it to T, and then invokes consumer with it. (Basically, it is a guard for consumer.)

This works more or less fine, I can write

Consumer<String> c = ...;
function(String.class, c);

Unfortunately, the following code does not work:

Consumer<List<String>> c = ...;
function(List.class, c);

This is because List.class has type Class<List>, not class Class<List<String>>. I can change the type of c to Consumer<List>, but that would be incompatible with my code in other places (and raw types are not nice anyway).

Is there some way to (a) invoke function in a way that typechecks or (b) change the type signature of function in a way that makes this pattern work?

Notes:

  • I wish to retain as much compile and runtime type safety as possible. So, function(Class<?> clazz, Consumer<T> consumer) would not be acceptable because the user can easily provide the wrong clazz. And invoking as function((Class<List<String>>)List.class, c) is also not acceptable because I could accidentally cast it as function((Class<Integer>)List.class, c) and this wrong cast would not even be caught at runtime. If there was a function that allows to cast Class<C<T>> to Class<C<U>> but not to Class<D>, that would be cool, but I don't see that without higher-order types.

  • I am aware that the type casts will not be able to check at runtime that we have a List<String> and not a List<Integer>. That's OK. (Well, it's not, but I assume it's the best we can expect in a JVM language.)

  • All being the same, I prefer solutions that make the invocation of function simple (possible making function more complex) because function is defined in my library and used by the user of that library.

  • For the curious, the actual code where I have this problem is here, Instance is function.

2 Answers

It's not possible to do what you want with just a Class<T> argument. You need a super type token instead.

As explained in the linked article, this is how you might implement such functionality:

public abstract class TypeToken<T> {
 
    private final Type type;
 
    public TypeToken() {
        Type superclass = getClass().getGenericSuperclass();
        type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
 
    public Type getType() {
        return type;
    }
}

In Java, you cannot grab info about generic type variables of instances, however you can get some info about the generic type variables of super types. Note that the TypeToken class is abstract. This is how you should use it in your example:

<T> void function(TypeToken<T> token, Consumer<T> consumer)

Consumer<List<String>> c = ...;

function(new TypeToken<List<String>>() { }, c);

This means that you are passing a subclass of TypeToken to your function.

Finally, inside function, you can extract a Class instance and type cast an object as follows:

Class<T> clazz;
if (type instanceof ParameterizedType)
  clazz = (Class<T>) ((ParameterizedType) type).getRawType();
else if (typeU instanceof Class)
  clazz = (Class<T>) type;
else
  throw new RuntimeException("Error handling omitted");
T t = clazz.cast(obj);

I don't think it's possible to achieve the degree of type safety you aim at.

Let's distinguish compile time and run time.

Compile Time

At compile time, the compiler checks whether the type of clazz matches the consumer's type parameter T. But that doesn't work well with types T that themselves have generics type parameters, e.g. List<String>, as there is no List<String>.class syntax in Java. So, here the best you can do is cast the List.class to Class<List<String>>.

The object obj that gets operated upon in your function isn't visible in the method's signature, so at compile time there's no chance to check whether its type matches the consumer.

There's nothing stopping you from calling function(String.class, stringConsumer) and having an Integer object. There's no chance the compiler will detect that mistake.

Run Time

At run time, there is no generics information. That concept is called "type erasure". So, something that at compile time appears as a List<String>, at runtime is just a List.

As at compile time, the obj is not visible at the function invocation, you moved the type check to run time, using the Class.cast() method.

If at compile time your obj is a List<String>, a List<Integer>, or a List<Whatever>, at run time it's just a raw List without parameters, and the class you are checking against also is just the raw List.class, without any type parameters. The cast() method will accept all these different lists.

So, at run time there's no way to reject a List<Integer> from entering a consumer expecting a List<String>. You'll probably get a ClassCastException later when the consumer accesses the list elements.

Summary

If you want to guard against mismatch of parameterized types, you can only do so at compile time, and to make that possible you have to bring object and consumer into the same syntactic context, probably meaning a big refactoring.

At run time, you can only guard against mismatch of raw types. If you want to do that, it forces you to use some ugly syntax in case of parameterized types (casting the class object to a parameterized class type), just to satisfy the compiler.

Personally, I'd give up the guarding idea.

  • It doesn't help at compile time (it only checks whether you were able to pass a clazz matching the consumer into function, not whether the actual object matches).

  • It can check for some error cases at run time. But in cases like List<String>, it still can't protect you from getting a ClassCastException later, thrown by the consumer. And that's what happens without your guard as well, just in more cases.

Related