【问题标题】:Ambiguous overload in Java8 - is ECJ or javac right?Java8 中的模棱两可的重载 - ECJ 或 javac 对吗?
【发布时间】:2014-12-01 02:00:09
【问题描述】:

我有以下课程:

import java.util.HashSet;
import java.util.List;

public class OverloadTest<T> extends  HashSet<List<T>> {
  private static final long serialVersionUID = 1L;

  public OverloadTest(OverloadTest<? extends T> other) {}

  public OverloadTest(HashSet<? extends T> source) {}

  private OverloadTest<Object> source;

  public void notAmbigious() {
    OverloadTest<Object> o1 = new OverloadTest<Object>(source);
  }

  public void ambigious() {
    OverloadTest<Object> o2 = new OverloadTest<>(source);
  }
}

这在 JDK 7 的 javac 以及 eclipse(合规性设置为 1.7 或 1.8)下编译得很好。但是,尝试在 JDK 8 的 javac 下编译时,出现以下错误:

[ERROR] src/main/java/OverloadTest.java:[18,35] reference to OverloadTest is ambiguous
[ERROR] both constructor <T>OverloadTest(OverloadTest<? extends T>) in OverloadTest and constructor <T>OverloadTest(java.util.HashSet<? extends T>) in OverloadTest match

请注意,此错误仅适用于ambigous() 方法中的构造函数调用,不适用于notAmbiguous() 方法中的构造函数调用。唯一的区别是ambiguous() 依赖于菱形运算符。

我的问题是:JDK 8 下的 javac 是否正确标记了不明确的解决方案,或者 JDK 7 下的 javac 是否未能捕捉到歧义?根据答案,我需要提交 JDK 错误或 ecj 错误。

【问题讨论】:

  • 我宁愿说一个 JDK 错误,因为它似乎破坏了向后兼容性!
  • 我不记得细节了,但如果我没记错的话,JDK 7 中有一个错误允许这样的事情发生,而 JDK 8 有更严格的检查。所以,是的,向后兼容性的突破,但是是故意的。
  • 有关更多详细信息,请参阅 Oracle 文档中的 Incompatibilities between JDK 8 and JDK 7
  • 这是 JDK 中的一个错误。这是最特定类型的简单重载解决方案,即。 OverloadTest,应该选择。

标签: java eclipse javac jls ecj


【解决方案1】:

在调用中,当显式设置 T 调用构造函数时,没有歧义:

OverloadTest<Object> o1 = new OverloadTest<Object>(source);

因为 T 是在构造函数调用时定义的,所以 Object 通过 ?在编译时扩展对象检查就好了,没有问题。当 T 显式设置为 Object 时,两个构造函数的选择变为:

public OverloadTest(OverloadTest<Object> other) {}
public OverloadTest(HashSet<Object> source) {}

在这种情况下,编译器很容易选择第一个。在另一个示例中(使用菱形运算符)T 没有显式设置,因此编译器首先尝试通过检查实际参数的类型来确定 T,而第一个选项不需要这样做。

如果更改第二个构造函数以正确反映我想象的所需操作(因为 OverloadTest 是 T 的列表的 HashSet,那么应该可以传入 T 的列表的 HashSet),如下所示:

public OverloadTest(HashSet<List<? extends T>> source) {}

...那么歧义就解决了。但就目前而言,当您要求编译器解决该模棱两可的调用时,就会出现冲突。

编译器将看到菱形运算符,并尝试根据传入的内容和各种构造函数的期望来解析 T。但是HashSet构造函数的写法会保证无论传入哪个类,两个构造函数都会保持有效,因为擦除之后,T总是被Object替换。而当 T 为 Object 时,HashSet 构造函数和 OverloadTest 构造函数具有相似的擦除,因为 OverloadTest 是 HashSet 的有效实例。而且因为一个构造函数没有覆盖另一个(因为 OverloadTest 没有扩展 HashSet),实际上不能说一个比另一个更具体,所以它不知道如何做出选择,反而会抛出编译错误。

这只是因为通过使用 T 作为边界,您正在强制编译器进行类型检查。如果你只是把它做成 > 而不是 extends T> 它可以编译得很好。 Java 8 编译器在类型和擦除方面比 Java 7 更严格,部分原因是 Java 8 中的许多新特性(如接口防御方法)要求它们对泛型更加迂腐。 Java 7 没有正确报告这些事情。

【讨论】:

  • 这个答案没有显示 JLS 如何要求编译器来标记歧义。更具体的是根据覆盖定义(请参阅 JLS 15.12.2.5 和 JLS 18.5.4(Java 8 中的新内容))。因此,我认为这个答案是不正确的
【解决方案2】:

很抱歉破坏了聚会,但 Java 中的重载解析比目前看到的 cmets 和答案更复杂。

两个构造函数都适用:

OverloadTest(OverloadTest<? extends T>)
OverloadTest(HashSet<? extends T>) 

此时,Java 8 使用了"More Specific Method Inference",它会分析所有候选对象对。

在检查前一个构造函数是否比后者更具体的过程中,成功解决了以下约束(T#0HashSet的类型参数T的推断变量):

OverloadTest<? extends T>  <: HashSet<? extends T#0>

解决方法是将T#0实例化为List&lt;T&gt;

反向尝试未能解决以下约束(这里 T#0OverloadTest 的类型参数 T 的推断变量):

HashSet<? extends T> <: OverloadTest<? extends T#0>

找不到满足该约束的 T#0 实例化。

由于一个方向成功而另一个方向失败,因此认为前者的构造函数比后者更具体,从而解决了歧义。

Ergo:接受程序似乎是编译器的正确答案。

PS:我并不是反对在 Java 8 中进行更多迂腐的类型检查在必要时,但事实并非如此。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-12
    • 1970-01-01
    • 1970-01-01
    • 2011-06-30
    • 1970-01-01
    • 2012-07-14
    相关资源
    最近更新 更多