Search code examples
javagenericsjava-8jersey-2.0hk2

Call generic method with class determined at runtime


I'm trying to automatically bind factory classes with a certain annotation using jersey 2/HK2. Therefore, I get the provided type at runtime from a generic interface and then try to bind the factory to this type. The method that binds the factory to a class looks like this:

protected void bindResourceFactory(Class<? extends Factory<?>> factory) {
  Class<?> providedClass = getProvidedClass(factory);
  bindFactory(factory).to(providedClass).in(Singleton.class);
}

The bindFactoy method provided by HK2 is defined as following:

public <T> ServiceBindingBuilder<T> bindFactory(Class<? extends Factory<T>> factoryType) {
    return resetBuilder(AbstractBindingBuilder.<T>createFactoryBinder(factoryType, null));
}

This seems to work well when I build everything with eclipse. However when I build the project with maven, I get the following build error:

[ERROR] /Users/jan/Documents/Workspace/jersey-test/bind/ResourceFactoryBinder.java:[32,5] no suitable method found for bindFactory(java.lang.Class<capture#1 of ? extends org.glassfish.hk2.api.Factory<?>>)
[ERROR]     method org.glassfish.hk2.utilities.binding.AbstractBinder.<T>bindFactory(java.lang.Class<? extends org.glassfish.hk2.api.Factory<T>>,java.lang.Class<? extends java.lang.annotation.Annotation>) is not applicable
[ERROR]       (cannot infer type-variable(s) T
[ERROR]         (actual and formal argument lists differ in length))
[ERROR]     method org.glassfish.hk2.utilities.binding.AbstractBinder.<T>bindFactory(java.lang.Class<? extends org.glassfish.hk2.api.Factory<T>>) is not applicable
[ERROR]       (cannot infer type-variable(s) T
[ERROR]         (argument mismatch; java.lang.Class<capture#1 of ? extends org.glassfish.hk2.api.Factory<?>> cannot be converted to java.lang.Class<? extends org.glassfish.hk2.api.Factory<T>>))
[ERROR]     method org.glassfish.hk2.utilities.binding.AbstractBinder.<T>bindFactory(org.glassfish.hk2.api.Factory<T>) is not applicable
[ERROR]       (cannot infer type-variable(s) T
[ERROR]         (argument mismatch; java.lang.Class<capture#1 of ? extends org.glassfish.hk2.api.Factory<?>> cannot be converted to org.glassfish.hk2.api.Factory<T>))

The java version in both cases is 1.8.0_152.

The reason probably is that my argument used is of type Class<? extends Factory<?>> whereas bindFactory expects Class<? extends Factory<T>>. Does someone know, why this might build with eclipse but not with maven? And is there any way to make this work apart from calling bindFactory via reflection?


Solution

  • The reason this error happens is because the compiler doesn't capture convert the "inner" wildcard in Class<? extends Factory<?>> (the one in Factory<?>). (In terms of the specification, "capture conversion is not applied recursively".)

    It's easier to explain why this should happen with a different (but analogous with respect to the kind of types involved) example. Suppose we have a List of any type of List:

    List<List<?>> lists = ...;
    

    Now suppose we have some method that processes lists of lists, but assumes that the lists all have the same type:

    <T> void process(List<List<T>> lists) {
        // and at this point we should note that List<T>
        // allows us to add elements to the lists, so we
        // could do something like this:
    
        if (!lists.isEmpty()) {
            List<T> list0 = lists.get(0);
    
            for (int i = 1; i < lists.size(); ++i)
                list0.addAll(lists.get(i));
        }
    }
    

    So the question is: should we be able to pass our List<List<?>> to the process method? Well, it could be that we've built our list of lists in something like the following way:

    List<Double> doubles = new ArrayList<>();
    Collections.addAll(doubles, 0.0, 1.0, 2.0);
    
    List<String> strings = new ArrayList<>();
    Collections.addAll(strings, "X", "Y", "Z");
    
    List<List<?>> lists = new ArrayList<>();
    Collections.addAll(lists, strings, doubles);
    

    In that case it's more obvious that we shouldn't be able to pass the List<List<?>> to the process method taking a List<List<T>>. The way this is actually accomplished by the compiler is that it won't capture the "inner" wildcard to some type variable T.

    The code in the question doesn't compile for a pretty similar reason. Since the type parameter on Class is mainly relevant to methods related to constructors (and in particular the newInstance method), we could show an example that's more similar using Supplier:

    static void example(Supplier<? extends Factory<?>> s) {
        capture(s);
    }
    static <T> void capture(Supplier<? extends Factory<T>> s) {
        Factory<T> a = s.get();
        Factory<T> b = s.get();
        // remember, a and b are supposed to have the same type
        T obj = a.provide();
        b.dispose(obj);
    }
    

    The problem is that since our supplier could originally be a Supplier<Factory<?>>, there's no reason it couldn't, say, return a Factory<String> from one invocation and a Factory<Double> from another. We therefore shouldn't be able to capture Supplier<Factory<?>> to Supplier<Factory<T>>. Class.newInstance will always return objects of the exact same type, but the compiler doesn't know that.

    I think that Eclipse's compiler is probably just wrong in this case to compile the code in the question.

    If you want to force this to compile, you could use unchecked casts as that user in the comments is suggesting, but I don't know enough about the classes involved to say whether the result of that is actually provably correct. The above two code examples show how doing something like that could actually go horribly wrong (in principle), but Class is sometimes a special case.

    A way to fix it that's more proper would be to declare a type variable on bindResourceFactory so it takes a Class<? extends Factory<T>> too, but I don't know if that actually works for the way you're calling the method.