使用运行时确定的 class 调用泛型方法

Call generic method with class determined at runtime

我正在尝试使用 jersey 2/HK2 将工厂 classes 与特定注释自动绑定。因此,我在运行时从通用接口获取提供的类型,然后尝试将工厂绑定到该类型。将工厂绑定到 class 的方法如下所示:

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

HK2提供的bindFactoy方法定义如下:

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

当我用 Eclipse 构建所有内容时,这似乎很有效。但是,当我使用 Maven 构建项目时,出现以下构建错误:

[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>))

两种情况下的java版本都是1.8.0_152.

原因可能是我使用的参数是 Class<? extends Factory<?>> 类型,而 bindFactory 期望 Class<? extends Factory<T>>。有人知道,为什么这可能是用 eclipse 而不是用 maven 构建的?除了通过反射调用 bindFactory 之外,还有什么方法可以使这项工作正常进行吗?

发生此错误的原因是因为编译器没有捕获转换 Class<? extends Factory<?>> 中的 "inner" 通配符(Factory<?> 中的通配符)。 (就规范而言,"capture conversion is not applied recursively"。)

用不同的(但在所涉及的类型方面类似)示例更容易解释为什么会发生这种情况。假设我们有一个 List 任何类型的 List:

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

现在假设我们有一些处理列表列表的方法,但假设所有列表都具有相同的类型:

<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));
    }
}

所以问题是:我们是否可以将 List<List<?>> 传递给 process 方法?好吧,可能是我们按照以下方式构建了我们的列表列表:

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);

在那种情况下,更明显的是我们不应该能够将 List<List<?>> 传递给采用 List<List<T>> 的处理方法。编译器实际实现的方式是它不会将 "inner" 通配符捕获到某些类型变量 T.

由于非常相似的原因,问题中的代码无法编译。由于 Class 上的类型参数主要与构造函数相关的方法相关(特别是 newInstance 方法),我们可以使用 Supplier:[=36= 来展示一个更相似的示例]

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);
}

问题在于,由于我们的供应商最初可能是 Supplier<Factory<?>>,所以没有理由不能,比如说,return 来自一次调用的 Factory<String>Factory<Double> 来自另一个。因此,我们不应该能够捕获 Supplier<Factory<?>>Supplier<Factory<T>>Class.newInstance 将始终 return 完全相同类型的对象,但编译器不知道。

我认为 Eclipse 的编译器在这种情况下可能只是错误地编译了问题中的代码。

如果你想强制编译它,你可以可以使用未经检查的强制转换,正如评论中的用户所建议的那样,但我对 类 涉及说结果是否真的可以证明是正确的。上面的两个代码示例展示了如何做这样的事情实际上可能会出错(原则上),但 Class 有时是一种特殊情况。

一种更合适的修复方法是在 bindResourceFactory 上声明一个类型变量,因此它也需要一个 Class<? extends Factory<T>>,但我不知道这是否真的适用于这种方式你正在调用方法。

发生这种情况是因为 eclipse 使用它自己的名为 ECJ 的编译器,而 maven 使用 javac 编译器。有时在 ECJ 中编译的代码不能在 javac 中编译,反之亦然。

在这种特殊情况下,eclipse 编译器能够推断泛型类型 T 但 javac 不能。所以你需要明确地告诉类型T,这是未知的,因为接收到的类型是Class<? extends Factory<?>>,这意味着你应该像下面这样使用Object

this.<Object>bindFactory((Class<? extends Factory<Object>>) factory);

在这种情况下需要强制转换工厂,并且可以省略 this.<Object> 因为编译器已经推断出 Object。

你终于可以抑制转换警告了,最好'uncheck'代码越少越好。

@SuppressWarnings({ "unchecked" })
Class<? extends Factory<Object>> objFactory = (Class<? extends Factory<Object>>) factory;
bindFactory(objFactory).to(providedClass).in(Singleton.class);

需要考虑的一件重要事情是方法 getProvidedClass(...) 应该 return class 正确

此外,在这样的方法中使用泛型 <T> void bindResourceFactory(Class<? extends Factory<T>>) 会再次将您带到同一个地方,因为您将无法使用 class 扩展 Factory<?> 带通配符 (Class<? extends Factory<?>>).