Java8中的模棱两可的重载 - ECJ或javac是对的吗?

2022-09-03 16:07:13

我有以下类:

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错误。


答案 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 列表的哈希集,那么传入 T 列表的哈希集应该是可能的),如下所示:

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

...然后消除歧义就解决了。但就目前而言,当您要求编译器解决该模棱两可的调用时,将会发生冲突。

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

发生这种情况只是因为使用 T 作为边界,您正在强制编译器执行类型检查。如果你只是让它<>而不是<?扩展T>它会编译得很好。Java 8编译器在类型和擦除方面比Java 7更严格,部分原因是Java 8中的许多新功能(如接口防御方法)要求它们在泛型上更加迂腐。Java 7 没有正确报告这些事情。


答案 2

很抱歉破坏了派对,但是Java中的过载解决方案比到目前为止看到的评论和答案更复杂。

两个构造函数都适用:

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

在这一点上,Java 8采用了“更具体的方法推理”,它分析了所有候选者对。

在检查前者构造函数是否比后者更具体时,成功解析了以下约束(T#0 是 类型参数的推理变量):THashSet

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

解决方案是将 T#0 实例化为 。List<T>

反向尝试无法解决以下约束(此处 T#0 是 类型参数的推理变量):TOverloadTest

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

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

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

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

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


推荐