泛型的引用不明确

2022-09-01 05:27:56

我在这里遇到了一个非常棘手的泛型和方法重载的情况。看看这个示例类:

public class Test {
    public <T> void setValue(Parameter<T> parameter, T value) {
    }

    public <T> void setValue(Parameter<T> parameter, Field<T> value) {
    }

    public void test() {
        // This works perfectly. <T> is bound to String
        // ambiguity between setValue(.., String) and setValue(.., Field)
        // is impossible as String and Field are incompatible
        Parameter<String> p1 = getP1();
        Field<String> f1 = getF1();
        setValue(p1, f1);

        // This causes issues. <T> is bound to Object
        // ambiguity between setValue(.., Object) and setValue(.., Field)
        // is possible as Object and Field are compatible
        Parameter<Object> p2 = getP2();
        Field<Object> f2 = getF2();
        setValue(p2, f2);
    }

    private Parameter<String> getP1() {...}
    private Parameter<Object> getP2() {...}

    private Field<String> getF1() {...}
    private Field<Object> getF2() {...}
}

上面的例子在Eclipse(Java 1.6)中完美地编译,但不是使用Ant javac命令(或JDK的javac命令),在那里我在第二次调用时收到这种错误消息:setValue

对 setValue 的引用是模棱两可的,两种方法 setValue(org.jooq.Parameter,T) 在 Test 中和 method setValue(org.jooq.Parameter,org.jooq.Field) 在 Test match 中

根据规范和我对Java编译器工作原理的理解,应该始终选择最具体的方法:http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#20448

无论如何,即使绑定到 ,这使得这两种方法都可以接受调用,但具有参数的方法似乎总是更具体。它适用于Eclipse,只是不适用于JDK的编译器。<T>ObjectsetValueField

更新

像这样,它可以在Eclipse和JDK编译器中工作(当然,带有rawtypes警告)。我理解,当涉及泛型时,规范中指定的规则非常特殊。但我发现这相当令人困惑:

    public <T> void setValue(Parameter<T> parameter, Object value) {
    }

    // Here, it's easy to see that this method is more specific
    public <T> void setValue(Parameter<T> parameter, Field value) {
    }

更新 2

即使使用泛型,我也可以创建此解决方法,通过添加一个名为 .这让我认为 to 的绑定才是真正导致这里所有麻烦的原因:<T>ObjectsetValuesetValue0TObject

    public <T> void setValue(Parameter<T> parameter, T value) {
    }

    public <T> void setValue(Parameter<T> parameter, Field<T> value) {
    }

    public <T> void setValue0(Parameter<T> parameter, Field<T> value) {
        // This call wasn't ambiguous in Java 7
        // It is now ambiguous in Java 8!
        setValue(parameter, value);
    }

    public void test() {
        Parameter<Object> p2 = p2();
        Field<Object> f2 = f2();
        setValue0(p2, f2);
    }

我在这里误解了什么吗?是否存在与此相关的已知编译器错误?或者是否有解决方法/编译器设置来帮助我?

后续:

对于那些感兴趣的人,我已经向Oracle和Eclipse提交了一份错误报告。甲骨文已经接受了这个错误,到目前为止,Eclipse已经分析了它并拒绝了它!看起来我的直觉是正确的,这是一个错误javac


答案 1

JDK是对的。第 2 种方法并不比第 1 种方法更具体。来自 JLS3#15.12.2.5

“非正式的直觉是,如果第一个方法处理的任何调用都可以在没有编译时类型错误的情况下传递给另一个方法,那么一种方法比另一个方法更具体。

这里的情况显然并非如此。我强调了任何调用。一种方法比另一种方法更具体的性质纯粹取决于这两种方法本身;它不会因调用而改变。

对你的问题进行正式分析:m2 比 m1 更具体吗?

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value) 

首先,编译器需要从初始约束中推断 R:

Parameter<V>   <<   Parameter<R>
Field<V>       <<   R

结果是,根据 15.12.2.7 中的推理规则R=V

现在我们替换并检查子类型关系R

Parameter<V>   <:   Parameter<V>
Field<V>       <:   V

根据 4.10.2 中的子类型规则,第 2 行不成立。因此,m2 并不比 m1 更具体。

V不在此分析中;分析考虑了 的所有可能值。ObjectV

我建议使用不同的方法名称。过载从来都不是必需品。


这似乎是Eclipse中的一个重大错误。规范非常清楚地表明,在此步骤中不会替换类型变量。Eclipse显然首先进行类型变量替换,然后检查方法特异性关系。

如果这种行为在某些例子中更“明智”,那么在其他例子中则不然。说

m1: <T extends Object> void check(List<T> list, T obj) { print("1"); }
m2: <T extends Number> void check(List<T> list, T num) { print("2"); }

void test()
    check( new ArrayList<Integer>(), new Integer(0) );

“直观地”,并且正式地按照规格,m2比m1更具体,测试打印“2”。但是,如果首先进行替换,则两种方法将变得相同!T=Integer


对于更新 2

m1: <R> void setValue(Parameter<R> parameter, R value) 
m2: <V> void setValue(Parameter<V> parameter, Field<V> value) 

m3: <T> void setValue2(Parameter<T> parameter, Field<T> value)
s4:             setValue(parameter, value)

在这里,m1 不适用于方法调用 s4,因此 m2 是唯一的选择。

根据15.12.2.2,为了查看m1是否适用于s4,首先,进行类型推断,得出R = T的结论;然后我们检查 ,这导致 ,这是假的。Ai :< SiField<T> <: T

这与前面的分析一致 - 如果m1适用于s4,那么m2处理的任何调用(本质上与s4相同)都可以由m1处理,这意味着m2将比m1更具体,这是错误的。

在参数化类型中

请考虑以下代码

class PF<T>
{
    public void setValue(Parameter<T> parameter, T value) {
    }

    public void setValue(Parameter<T> parameter, Field<T> value) {
    }
}

void test()

    PF<Object> pf2 = null;

    Parameter<Object> p2 = getP2();
    Field<Object> f2 = getF2();

    pf2.setValue(p2,f2);

这可以毫无问题地编译。根据 4.5.2,中的方法类型是 带有替换的方法。也就是说,方法PF<Object>PF<T>T=Objectpf2

    public void setValue(Parameter<Object> parameter, Object value) 

    public void setValue(Parameter<Object> parameter, Field<Object> value) 

第 2 种方法比第 1 种方法更具体。


答案 2

我的猜测是编译器正在按照 JLS 第 15.12.2.5 节执行重载解析的方法。

对于本节,编译器使用强子类型(因此不允许任何未经检查的转换),因此,成为并成为 .以下规则将适用:T valueObject valueField<T> valueField<Object> value

当且仅当以下两个条件都成立时,方法 m 才可通过子类型应用:

* For 1in, either:
      o Ai is a subtype (§4.10) of Si (Ai <: Si) or
      o Ai is convertible to some type *Ci* by unchecked conversion

(§5.1.9)和 Ci <: Si. * 如果 m 是如上所述的通用方法,则 Ul <: Bl[R1 = U1, ..., Rp = Up], 1lp.

(请参阅项目符号 2)。既然是子类型,那么就找到了最具体的方法。字段匹配您的两种方法(由于上面的项目符号 2),并使其不明确。Field<Object>Objectf2

对于 和 ,两者之间没有子类型关系。StringField<String>

PS.这是我对事物的理解,不要把它引用为犹太洁食。