34

考虑以下代码:

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

现在,我可以理解为什么它会被认为是模棱两可的(因为int可以自动装箱到 中Integer,从而匹配 的条件或缺少条件K)。但是,我无法理解的是,如果您使用String[]而不是String....

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

4

4 回答 4

15

您的第一个案例非常简单。下面的方法:

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

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

第一阶段(§15.12.2.2)在不允许装箱或拆箱转换的情况下执行重载解决方案

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


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

在您的第二种情况下,由于您使用了 var-args,因此将使用 var-args 和装箱或拆箱来完成方法重载解析,如该 JLS 部分中解释的第三阶段:

第三阶段(第 15.12.2.4 节)允许将重载与可变参数方法、装箱和拆箱相结合。

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

第二阶段(第 15.12.2.3 节)执行重载决议,同时允许装箱和拆箱,但仍排除使用变量 arity 方法调用。

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

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

编译器应该已经从 LHS推断出K泛型方法的类型为 , 。IntWrapper但它似乎是在推断它是K一种Integer类型,因此你的两种方法现在都同样适用于这个方法调用,因为这两个 requiresvar-argsboxing.

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

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

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


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

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

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

现在,类型K将被推断为IntWrapper,但由于1不可转换为IntWrapper,因此该方法被丢弃,第二个方法将被调用,它会正常工作。


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

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,这是关于编译器如何推断类型参数的,但这完全超出了我的想象。

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


事实证明,Compiler 没有玩任何花招:

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

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

于 2013-08-27T09:24:44.200 回答
3

给你,下面两种方法的区别: 方法1:

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

方法二:

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(""));
 }

这就是为什么你会模棱两可..

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

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

如果要检查有效性,请尝试将整数作为第一个参数传递,然后传递字符串的可变参数。你会看到不同之处。

例如:

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");
}
}

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

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

于 2013-08-27T08:27:25.387 回答
3

在这种情况下

(1) m(K,   String[])
(2) m(int, String[])

m(1, new String[]{..});

m(1) 满足15.12.2.3。阶段 2:识别方法调用转换适用的匹配 Arity 方法

m(2) 满足15.12.2.2。第 1 阶段:识别子类型适用的匹配 Arity 方法

编译器在阶段 1 停止;它发现 m(2) 是该阶段唯一适用的方法,因此选择了 m(2)。

在 var arg 情况下

(3) m(K,   String...)
(4) m(int, String...)

m(1, str1, str2);

m(3) 和 m(4) 都满足15.12.2.4。第 3 阶段:确定适用的可变 Arity 方法。两者都不比另一个更具体,因此模棱两可。

我们可以将适用的方法分为 4 组:

  1. 通过子类型适用
  2. 通过方法调用转换适用
  3. 可变参数,通过子类型应用
  4. vararg,通过方法调用转换适用

规范合并了第 3 组和第 4 组,并在第 3 阶段处理它们。因此不一致。

他们为什么这样做?Maye他们只是厌倦了它。

另一个批评是,不应该有所有这些阶段,因为程序员不会那样思考。我们应该简单地不加选择地找到所有适用的方法,然后选择最具体的一种(使用一些机制来避免装箱/拆箱)

于 2013-08-30T15:08:09.167 回答
0

首先,这只是一些初步线索......可能会编辑更多。

编译器总是搜索并选择最具体的可用方法。虽然读起来有点笨拙,但它们都在JLS 15.12.2.5中指定。因此,通过调用

转换器.pack(1, "Test", "Test2" )

编译器无法确定 是否1应解散为Kint。换句话说,K 可以适用于任何类型,因此它与 int/Integer 处于同一级别。

区别在于参数的数量和类型。考虑这new String[]{"Test", "Test2"}是一个数组,"Test", "Test2"而是两个字符串类型的参数!

转换器.pack(1); // 模棱两可,编译器错误

转换器.pack(1, null); // 调用方法 2,编译器警告

转换器.pack(1, 新字符串[]{}); // 调用方法 2,编译器警告

converter.pack(1, new Object());// 模棱两可,编译错误

converter.pack(1, new Object[]{});// 调用方法2,没有警告

于 2013-08-27T08:55:53.347 回答