Java:泛型方法重载歧义

请考虑以下代码:

public class Converter {

    public <K> MyContainer<K> pack(K key, String[] values) {
        return new MyContainer<>(key);
    }

    public MyContainer<IntWrapper> pack(int key, String[] values) {
        return new MyContainer<>(new IntWrapper(key));
    }


    public static final class MyContainer<T> {
        public MyContainer(T object) { }
    }

    public static final class IntWrapper {
        public IntWrapper(int i) { }
    }


    public static void main(String[] args) {
        Converter converter = new Converter();
        MyContainer<IntWrapper> test = converter.pack(1, new String[]{"Test", "Test2"});
    }
}

上面的代码编译没有问题。但是,如果同时在签名和 中都更改为 ,编译器会抱怨调用是不明确的。String[]String...packnew String[]{"Test", "Test2"}"Test", "Test2"converter.pack

现在,我可以理解为什么它可以被认为是模棱两可的(可以自动装箱到一个,从而符合或缺乏条件)。但是,我无法理解的是,如果您使用而不是.,为什么不存在歧义。intIntegerKString[]String...

有人可以解释一下这种奇怪的行为吗?


答案 1

您的第1案例非常简单。以下方法:

public MyContainer<IntWrapper> pack(int key, Object[] values) 

是参数的完全匹配 - 。来自 JLS 第 15.12.2 节(1, String[])

第一阶段 (§15.12.2.2) 执行过载解析,不允许装箱或取消装箱转换

现在,将这些参数传递给第二种方法时不涉及装箱。就像是一种超级类型的.甚至在Java 5之前,传递参数参数也是一个有效的调用。Object[]String[]String[]Object[]


编译器似乎在你的第二种情况下玩把戏:

在第二种情况下,由于您已经使用了var-args,因此方法重载解析将使用var-args和装箱或取消装箱来完成,如JLS部分中所述的第3阶段:

第三阶段 (§15.12.2.4) 允许将重载与可变 arity 方法、装箱和取消装箱相结合。

请注意,由于使用了 var-args,因此第 2 阶段在这里不适用:

第二阶段 (§15.12.2.3) 在允许装箱和取消装箱的同时执行重载解析,但仍排除使用可变 arity 方法调用。

现在这里发生的事情是编译器没有正确推断类型参数*(实际上,它正确地推断了它,因为类型参数被用作正式参数,请参阅此答案末尾的更新)。因此,对于您的方法调用:

MyContainer<IntWrapper> test = converter.pack(1, "Test", "Test2");

编译器应该从 LHS 推断出泛型方法的类型为 。但是,它似乎推断出是一种类型,因此您的两个方法现在都同样适用于此方法调用,因为两者都需要或.KIntWrapperKIntegervar-argsboxing

但是,如果该方法的结果没有分配给某个引用,那么我可以理解编译器无法推断出正确的类型,因为在这种情况下,is是完全可以接受给出歧义错误的:

converter.pack(1, "Test", "Test2");

可能是我猜想,只是为了保持一致性,它也被标记为第一种情况的模棱两可。但是,我再次不太确定,因为我没有从JLS或其他官方参考资料中找到任何可靠的来源来讨论这个问题。我会继续搜索,如果我找到一个,会更新答案。


让我们通过显式类型信息欺骗编译器:

如果更改方法调用以提供显式类型信息:

MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2");

现在,类型将被推断为 ,但由于不可转换为 ,该方法将被丢弃,并且将调用第二种方法,并且它将完全正常工作。KIntWrapper1IntWrapper


坦率地说,我真的不知道这里发生了什么。我希望编译器在第一种情况下也能从方法调用上下文中推断类型参数,因为它适用于以下问题:

public static <T> HashSet<T> create(int size) {  
    return new HashSet<T>(size);  
}
// Type inferred as `Integer`, from LHS.
HashSet<Integer> hi = create(10);  

但是,在这种情况下,它没有这样做。所以这可能是一个错误。

*或者可能是我不确切理解编译器如何推断类型参数,当类型不作为参数传递时。因此,为了更多地了解这一点,我尝试了 - JLS §15.12.2.7JLS §15.12.2.8,这是关于编译器如何推断类型参数的,但这完全超出了我的头顶。

所以,现在你必须忍受它,并使用替代方案(提供显式类型参数)。


事实证明,编译器没有玩任何把戏:

正如@zhong.j.yu.在注释中最后解释的那样,编译器仅将第15.12.2.8节应用于类型推断,当它无法按照15.12.2.7节推断它时。但在这里,它可以从传递的参数中推断出类型,因为很明显,类型参数是方法中的格式参数。Integer

因此,yes 编译器正确地将类型推断为 ,因此歧义是有效的。现在我认为这个答案是完整的。Integer


答案 2

在这里,您看以下两种方法之间的区别:方法1:

   public MyContainer<IntWrapper> pack(int key, Object[] values) {
    return new MyContainer<>(new IntWrapper(""));
   }

方法 2:

public MyContainer<IntWrapper> pack(int key, Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
}

方法 2 与

public MyContainer<IntWrapper> pack(Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
 }

这就是为什么你会得到一个模棱两可的原因。

编辑是的,我想说它们对于编译是相同的。使用变量参数的整个目的是使用户能够在他/她不确定给定类型的参数数量时定义方法。

因此,如果您使用对象作为变量参数,您只需说编译器我不确定我将发送多少个对象,另一方面,您说,“我正在传递一个整数和未知数量的对象”。对于编译器,整数也是一个对象。

如果要检查有效性,请尝试传递一个整数作为第一个参数,然后传递 String 的变量参数。你会看到区别。

例如:

public class Converter {
public static void a(int x, String... y) {
}

public static void a(String... y) {
}

public static void main(String[] args) {
    a(1, "2", "3");
}
}

另外,请不要互换使用数组和变量参数,它们有一些完全不同的目的。

当您使用 varargs 时,该方法不需要数组,而是需要相同类型的不同参数,这些参数可以通过索引方式进行访问。