How do I use Java generic wildcards with methods taking more than one generic parameter?

3k views Asked by At

So we have a generic method like this, which is part of dependency injection initialisation:

public static <TS, TI extends TS> void registerTransient(
    Class<TS> serviceClass, Class<TI> implementationClass)
{
    //
}

At some point we found a case where a class might not necessarily be present. And it's an implementation class which we would be injecting multiple off (so the service class is the same as the implementation class.) Naturally you would write this like this:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz, clazz);

IDEA has no problems with this, but javac complains:

error: method registerTransient in class TestTrash cannot be applied to given types;
required: Class<TS>,Class<TI>
found: Class<CAP#1>,Class<CAP#2>
reason: inferred type does not conform to declared bound(s)
inferred: CAP#2
bound(s): CAP#1
where TS,TI are type-variables:
TS extends Object declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
TI extends TS declared in method <TS,TI>registerTransient(Class<TS>,Class<TI>)
where CAP#1,CAP#2 are fresh type-variables:
CAP#1 extends Object from capture of ?
CAP#2 extends Object from capture of ?

What gives? The method requires the second parameter to be a subclass of the first. Irrespective of what class ? happens to be, it's the same class object for both parameters and a class is, I thought, always assignable from itself. It's almost as if javac is unnecessarily inventing a second wildcard type to use for the second parameter and then going "oh dear, you have two wildcards here, so I can't tell if one is assignable from the other."

2

There are 2 answers

2
lsoliveira On

The problem is that Class<?> cannot be cast to any other type unless via an explicit cast (it actually becomes Class<#capture-... of ?> during capture conversion). As such, the compiler cannot (statically) match the type bounds of those captures with the parameterized types of the method definition.

Try casting it explicitly to Class first, like in:

registerTransient((Class<Object>)clazz, clazz); 

This way the compiler can bind TS to Object and TI to something that extends Object (it will still emit a warning, though).

The fact that IntelliJ's compiler does not complain about this, might be due to some optimization or might even be a compiler bug. You should post it as such and wait for a reply.

If you wish to check it with a slightly different approach, the following will still not compile, even though it "looks" ok:

public class A {
    static class B {}

    static class C extends B {}

    static <T, R extends T> void method(final Class<T> t, final Class<R> r) {}

    public static final void main(String... args) {
        B b = new B();
        C c = new C();
        Class<?> cb = b.getClass();
        Class<?> cc = c.getClass();
        method(cb, cc);
    }
}

Have a look in here. It presents an extraordinary view of Java's type system (although quite dense).

0
Trevor Robinson On

The issue you're having is that the capture conversion happens individually for each method argument according to JLS 7 §6.5.6.1 Simple Expression Names:

If the expression name appears in a context where it is subject to assignment conversion or method invocation conversion or casting conversion, then the type of the expression name is the declared type of the field, local variable, or parameter after capture conversion (§5.1.10).

In your case, the "expression name" is the identifier clazz. As the compiler output shows, it is being captured twice, as required by the JLS.

The common technique for dealing with this is to introduce a helper method that binds the wildcard to a type variable:

private static <T> void registerTransient(Class<T> serviceAndImplClass)
{
    registerTransient(serviceAndImplClass, serviceAndImplClass);
}

Calling this new method with a wildcard will work:

Class<?> clazz = Class.forName("com.acme.components.MyPersonalImplementation");
registerTransient(clazz);

I see that you mentioned this workaround in a comment. It may seem strange, but it's actually the approach intended by the language designers.