告诉编译器一个<Object>与它所需的<?>是等价的。

4

我有一些对象,它们预先生成一些配置,以便之后能更快地处理计算(可能多次)。我正在尝试通用化它,以避免每次传递配置时都将其作为 Object 并进行转换。

interface IComputable<T> {
    T configure(); // Generate configuration object
    int compute(T conf); // Run the computation based on the pre-generated configuration
    float precision(T conf); // Make the compute() computation finer-grained
    ...
}

class ComputableInfo {
    IComputable<?> computable;
    Object config; // Real type is <?>
    int result;

    ComputableInfo(String id) {
        computable = ComputableFactory.createFrom(id);
        config = computable.configure();
        result = computable.compute(config); // <<<--- The method compute(capture#3-of ?) in the type TestInterface.IComputable<capture#3-of ?> is not applicable for the arguments (Object)
    }
}

我得到一个编译错误:

在TestInterface.IComputable<capture#3-of ?>类型中,方法compute(capture#3-of ?)对于参数(Object)不适用

当然,我可以将 int compute(T conf) 替换为 int compute(Object conf) 但是我需要显式地将其强制转换为适当的 T。这并不是很大的问题,但会使代码不够明显。

我也可以让 ComputableInfo 成为泛型:

interface ComputableInfo<T> {
    IComputable<T> computable;
    T config;
    ...

但是这样会在其他一些地方(主要是“原始类型”警告)引起编译问题,我希望避免比之前的解决方法更多的问题(使用T的替代方法是使用Object)。

有没有办法实现这一点?我甚至愿意将这种问题从错误转变为编译器设置中的警告,或者也许有一个额外的私有方法,可以返回包含configresult的单个对象?

编辑:关于如果我使ComputableInfo成为泛型的“进一步编译问题”,我在接口中有另一个方法(请参见编辑后的内容),该方法通过ComputableInfo调用:

ComputableInfo<?> info = getInfo(id);
info.computable.precision(info.config); // <<<--- (same kind of error)

问题在于ComputableInfo无法知道Computable<T>的类型T(至少我不知道如何知道),因为它来自一个从配置文件构建的工厂。

3
ComputableInfo 类型变成泛型类型是正确的解决方案。修复其他警告即可。 - chrylis -cautiouslyoptimistic-
2
你得到编译错误的原因是因为 IComputable<?> 是一个持有某些未知类型的 IComputable,你不能传递任意的 Object 给它,因为编译器无法检查该对象是否为正确的类型。这与你不能向 List<?> 添加元素的原因相同,在 SO 上已经被问答过很多次了。你可以将你的 IComputable<?> 强制转换为 IComputable<Object>,但这是一种丑陋和不安全的类型操作。 - Jesper
@Jesper,我确实理解编译器为什么会抱怨它。我已经详细说明了有关工厂创建可计算对象、擦除参数化的一些内容... - Matthieu
2
IComputable<? super Object> computable; 这个怎么样? - Nikolas Charalambidis
@Nikolas,这个方法很有效!如果你能把它作为答案添加进去,我会接受的 :) 如果你能解释一下为什么,我也会点赞的 :) (或者这可能是另一个问题...) - Matthieu
显示剩余2条评论
2个回答

4

从通配类型中获取对象并将其传回相同的对象是泛型类型系统的已知限制。例如,当您有以下代码时:

List<?> list = …

你可能希望将一个元素从一个索引复制到另一个索引,比如:

Object o = list.get(0);
list.set(1, o);

但是它不起作用,即使你避免了非指示类型的局部变量。换句话说,即使以下代码也无法编译:

list.set(1, list.get(0));

但是你可以添加一个通用的帮助方法,通过允许在操作期间捕获通配符类型来执行该操作:

static <T> void copyFromTo(List<T> l, int from, int to) {
    l.set(to, l.get(from));
}

List<?> list = …
copyFromTo(list, 0, 1); // now works

你也可以将这个模式应用到你的情况中:
class ComputableInfo {
    IComputable<?> computable;
    Object config; // Real type is <?>
    int result;

    ComputableInfo(String id) {
        computable = ComputableFactory.createFrom(id);
        configureAndCompute(computable);
    }

    private <T> void configureAndCompute(IComputable<T> computable) {
        T typedConfig = computable.configure();
        this.config = typedConfig;
        this.result = computable.compute(typedConfig);
    }
}

这个方法可行,而且不需要将ComputableInfo泛型化。

如果你需要长时间捕获类型,比如你想多次使用创建的config,你可以使用封装:

class ComputableInfo {
    static final class CompState<T> {
        IComputable<T> computable;
        T config;

        CompState(IComputable<T> c) {
            computable = c;
        }
        private void configure() {
            config = computable.configure();
        }
        private int compute() {
            return computable.compute(config);
        }
    }
    CompState<?> state;
    int result;

    ComputableInfo(String id) {
        state = new CompState<>(ComputableFactory.createFrom(id));
        state.configure();
        result = state.compute();
    }
}

这样,您仍然避免将类型参数导出给ComputableInfo的用户。


我最终使用了 State<T> 类的“技巧”:确实,当 <T> 对于可计算性有意义时,我们需要将逻辑移动到一个泛型内部。 - Matthieu
@Holger ... 或者使用原始类型,例如 Collections::reverse 所做的那样,但我认为这很丑陋。 - Eugene
2
@Eugene 我想到的是 Collections::swap 方法,但我在这个回答中进行了简化。这两种方法都包含了一个注释(在 OpenJDK 版本中),提到“补充私有方法”可以作为使用原始类型的替代方案。我认为,在初始化过程中使用冷代码路径中使用的核心类中,避免这样的委派是可以的,但应用程序代码不应该复制这种做法。 - Holger

1
你需要使用下限通配符。 Object 本身不符合通配符 ?
class ComputableInfo {
    IComputable<? super Object> computable;
    Object config;
    int result;

    ComputableInfo(String id) {
         computable = null;
         config = computable.configure();
         result = computable.compute(config);
    }
}

一个下界声明了,IComputable将是Object的一个实例或者是一些对象的超类的实例,而这个对象实际上是所有Objects的父对象。为了更好的理解,我们使用Number代替。
IComputable<Integer> computableInteger = ...;
IComputable<Number> computableNumber = ...;
IComputable<Object> computableObject = ...;

IComputable<? super Number> computableSuperNumber = ...;
computableSuperNumber = computableInteger;                  // doesn't compile
computableSuperNumber = computableNumber;                   // ok
computableSuperNumber = computableObject;                   // ok

然而,通过方法传递一个IntegerDouble是安全的。在下面的片段中,computableSuperObject引用了一个可能是以下之一的IComputable
  • IComputable<Number>
  • IComputable<Object>
由于引用可能是IComputable<Number>,使用Object进行计算是不合法的,只要它不适合那里,只要Object可以是例如String
IComputable<? super Number> computableSuperNumber = ...;

Integer integer = 1;
Double d = 1d;
Number number = 1;
Object object = 1;                     // the Object can be also "string", see below
Object objectString = "string";
String string = "string";

computableSuperNumber.compute(integer);                     // ok
computableSuperNumber.compute(d);                           // ok
computableSuperNumber.compute(number);                      // ok
computableSuperNumber.compute(object);                      // doesn't compile
computableSuperNumber.compute(objectString);                // doesn't compile
computableSuperNumber.compute(string);                      // doesn't compile

谢谢您的解释。然而,在您的第一个例子中,您说 computableSuperNumber = computableInteger 不能编译,但是 computableSuperNumber = computableObject 可以。难道不应该是相反的吗? - Matthieu
1
代码片段是正确的(尝试自己编译)。下限/上限通配符概念一开始很令人困惑,需要一段时间才能理解。IComputable<? super Number> 要么是 IComputable<Number> 要么是 IComputable<Object>,因此 computableSuperNumber = computableInteger; 无法编译。当您使用通配符参数调用方法时,您需要确保传递的类型是兼容的,因此传递 IntegerDoubleNumber 的子类型)和 Number 本身是可以的,而 StringObject 则不行。 - Nikolas Charalambidis
好的,谢谢。在这个概念深入我脑海之前,我需要花点时间理解它 ;) - Matthieu
2
当你有一个 IComputable<? super Number> 时,你可以将 Number 或任何其超类型传递给 compute 方法,但另一方面,你不能期望从 configure 中获得一个 Number。构造 IComputable<? super Object> 更奇怪,它确实能够工作,因为你只能实现一个实际兼容的类型,即 IComputable<Object>(或者你知道其他 Object 的超类型),所以它基本上与放弃泛型的好处没有什么不同。可分配的 IComputable 实现必须在它们的 compute 方法中接受任何 Object... - Holger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接