引用与泛型不明确

Posted

技术标签:

【中文标题】引用与泛型不明确【英文标题】:Reference is ambiguous with generics 【发布时间】:2011-07-18 16:55:24 【问题描述】:

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

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) 在测试和方法 setValue(org.jooq.Parameter,org.jooq.Field) 在测试比赛中

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

在任何情况下,即使&lt;T&gt; 绑定到Object,这使得setValue 方法都可以接受调用,但带有Field 参数的方法似乎总是更具体。它可以在 Eclipse 中工作,只是不适用于 JDK 的编译器。

更新

像这样,它可以在 Eclipse 和 JDK 编译器中工作(当然,带有 rawtypes 警告)。我知道,当涉及泛型时,the specs 中指定的规则非常特殊。但我觉得这很令人困惑:

    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

即使使用泛型,我也可以创建这个解决方法,通过添加一个名为 setValue0 的附加明确间接寻址,避免在 setValue 调用时将类型 &lt;T&gt; 绑定到 Object。这让我觉得 TObject 的绑定确实是造成所有麻烦的原因:

    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 提交了错误报告。 Oracle 已经接受了这个 bug,到目前为止,Eclipse 已经分析并拒绝了它!看来我的直觉是对的,这是javac中的一个错误

http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7031404 https://bugs.eclipse.org/bugs/show_bug.cgi?id=340506 https://bugs.eclipse.org/bugs/show_bug.cgi?id=469014(Eclipse Mars 的新问题)

【问题讨论】:

我认为,ant 指向的是不同的编译器,它不是候选版本。 @John:ant 使用 JDK 的 javac 编译器。 Eclipse 可能有它自己的... 【参考方案1】:

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

“非正式的直觉是,如果第一个方法处理的任何调用都可以传递给另一个方法而不会出现编译时类型错误,那么一个方法比另一个方法更具体。” p>

这显然不是这里的情况。我强调了任何调用。一种方法比另一种更具体的属性完全取决于这两种方法本身;每次调用都不会改变。

对你的问题的正式分析: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 中的子类型规则,第二行不成立。所以 m2 并不比 m1 更具体。

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

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


这似乎是 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 :&lt; Si,这导致Field&lt;T&gt; &lt;: 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&lt;Object&gt; 中的方法类型是 PF&lt;T&gt; 中的方法,替换为 T=Object。也就是pf2的方法是

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

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

第二种方法比第一种更具体。

【讨论】:

@irreputable,在这种情况下避免重载是正确的。由于情况太复杂,我无法理解,这是最安全的方法。不过,我不明白你的烦恼。 Eclipse 由 IBM 提供,javac 由 Sun/Oracle 提供。我确信规范本身是正确的,我从不怀疑。但是 javac 对我来说只是该规范的另一个实现。即使你的论点是有道理的,它也可能是相反的。毕竟,您自己发现了一个 javac 错误,正如我从您的 *** 问题中看到的那样...... @irreputable,还有一件事让我担心。如果V 绑定到Object 并不像你说的那么重要,那么为什么我的问题中的 UPDATE 2 是可编译的? @lukas-eder 看到我对你的更新 2 的补充。我同意如此详细的研究不值得花时间,除非是为了好玩。关于 javac,它不仅仅是另一个实现。它是由同一个人与规范同时开发的;一些功能首先在 javac 中实现,然后复制到规范中。其他编译器没有这种奢侈。 @irreputable:您的添加是有道理的。所有这一切都只是一般的噩梦。我仍然认为 Eclipse 的编译器在这种情况下表现得“更好”,因为它可能包含一个额外的(根据规范是错误的)消歧规则,考虑到 Object 绑定到 T 存在歧义根据规格。但这是我非常非正式和直观的意见,作为编译器反专家:-)。 我还没有看到完全符合 JLS3 的完全一致的解释,但是在 JLS8 中情况要清楚得多,在这个级别上 javac 和 ecj 同意报告歧义。【参考方案2】:

我的猜测是编译器正在按照JLS, Section 15.12.2.5 进行方法重载解析。

对于本节,编译器使用strong subtyping(因此不允许任何未经检查的转换),因此,T value 变为 Object valueField&lt;T&gt; value 变为 Field&lt;Object&gt; 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 u1 ...>

(请参阅项目符号 2)。由于Field&lt;Object&gt;Object 的子类型,因此找到了最具体的方法。字段 f2 匹配您的两种方法(因为上面的项目符号 2)并使其模棱两可。

对于StringField&lt;String&gt;,两者之间没有子类型关系。

PS。这是我对事物的理解,不要引用它作为犹太教。

【讨论】:

对重载方法的调用是在编译时解决的,不是吗?另外,为什么String 案例有效? 那么如果T 已经被删除,为什么我在编译器错误消息中有T?此外,ObjectField 仍然是不同的类型,即使在类型擦除之后也是如此 谢谢,我的第一个回答没有意义。我希望这会有所帮助。 如果转换未经检查,我们会看到编译时警告,不是吗?我认为事情比这更复杂。【参考方案3】:

编辑:这个答案是错误的。看看接受的答案。

我认为问题归结为:编译器没有将 f2 的类型(即 Field)和形式参数的推断类型(即 Field -> Field)视为同一类型。

换句话说,看起来f2(Field)的类型被认为是形参Field(Field)类型的子类型。由于 Field 是 Object 的子类型,因此编译器无法选择一种方法。

编辑:让我稍微扩展一下我的陈述

这两种方法都是applicable,看起来Phase 1: Identify Matching Arity Methods Applicable by Subtyping 用于决定调用哪个方法,而不是应用来自Choosing the Most Specific Method 的规则,但由于某种原因未能选择第二种方法而不是第一种方法。

第 1 阶段 部分使用此表示法:X &lt;: S(X 是 S 的子类型)。根据我对&lt;: 的理解,X &lt;: X 是一个有效的表达式,即&lt;: 并不严格,并且在此上下文中包含类型本身(X 是 X 的子类型)。这解释了第 1 阶段的结果:两种方法都被选为候选方法,因为 Field&lt;Object&gt; &lt;: ObjectField&lt;Object&gt; &lt;: Field&lt;Object&gt;

选择最具体的方法部分使用相同的符号表示一种方法比另一种更具体。有趣的部分是以“一个名为 m 的固定数量成员方法比另一个成员更具体......”开头的段落。除其他外,它具有:

对于从 1 到 n 的所有 j,Tj <: sj.>

这让我觉得在我们的例子中,第二种方法必须选择第一种方法,因为以下成立:

Parameter&lt;Object&gt; &lt;: Parameter&lt;Object&gt; Field&lt;Object&gt; &lt;: Object

而由于Object &lt;: Field&lt;Object&gt; 为假(对象不是字段的子类型)而反过来不成立。

注意:对于字符串示例,阶段 1 将简单地选择唯一适用的方法:第二个。

所以,回答您的问题:我认为这是编译器实现中的一个错误。 Eclipse 有它自己的增量编译器,它似乎没有这个错误。

【讨论】:

能否举例说明 举个例子到底是什么? @GeorgyBolyuba:如果你的答案是错误的:你为什么不删除它?

以上是关于引用与泛型不明确的主要内容,如果未能解决你的问题,请参考以下文章

集合框架与泛型

泛型编程类型约束与软件扩展性--面向可扩展的泛型编程就是面相类型约束编程

Java这个泛型不太正经

java遗珠之泛型不可靠类型

java遗珠之泛型不可靠类型

Java_集合与泛型