Java:重载歧义的通用方法

时间:2013-08-27 08:09:00

标签: java generics overloading variadic-functions

请考虑以下代码:

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...pack中将new String[]{"Test", "Test2"}更改为"Test", "Test2",则编译器会抱怨对converter.pack的调用不明确的。

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

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

4 个答案:

答案 0 :(得分:14)

你的1 st 案件非常简单。以下方法:

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

与参数完全匹配 - (1, String[])。来自JLS Section 15.12.2

  

第一阶段(§15.12.2.2)执行重载解析而不允许装箱或拆箱转换

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


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

在你的第二种情况下,既然你已经使用了var-args,那么方法重载分辨率将使用var-args,装箱或拆箱来完成,按照JLS部分中解释的第3阶段:

  

第三阶段(§15.12.2.4)允许重载与变量arity方法,装箱和拆箱相结合。

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

  

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

现在发生的事情是编译器没有正确地推断出类型参数* (实际上,它正确推断它,因为type参数被用作形式参数,见这个答案结束时的更新)。所以,对于你的方法调用:

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

编译器应该从LHS推断泛型方法中K的类型为IntWrapper。但似乎它推断KInteger类型,因此您的方法现在同样适用于此方法调用,因为它们都需要var-args或{{ 1}}。

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

boxing

可能我想,为了保持一致性,第一种情况也显得模棱两可。但是,我并不确定,因为我没有从JLS或其他官方参考资料中找到任何有关此问题的可信来源。我将继续搜索,如果我找到一个,将更新答案。


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

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

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

现在,类型MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2"); 将被推断为K,但由于IntWrapper无法转换为1,因此该方法将被废弃,第二种方法将被调用它会完美地运作。


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

IntWrapper

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

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

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


事实证明,Compiler没有玩任何技巧:

正如@ zhong.j.yu。在评论中最后解释的那样,编译器仅在第15.12.2.8节中对类型推断应用,当它无法根据15.12.2.7部分推断它时。但是在这里,它可以从传递的参数中将类型推断为public static <T> HashSet<T> create(int size) { return new HashSet<T>(size); } // Type inferred as `Integer`, from LHS. HashSet<Integer> hi = create(10); ,因为类型参数显然是方法中的格式参数。

所以,是的编译器正确地将类型推断为Integer,因此歧义是有效的。现在我觉得这个答案已经完成了。

答案 1 :(得分:3)

在这里,以下两种方法的区别: 方法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(""));
 }

这就是为什么你会有歧义......

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

因此,如果你使用一个对象作为变量参数,你只需要说编译器,我不确定我将发送多少个对象,另一方面,你说,&#34;我传递一个整数,未知数量的对象&#34;。对于编译器,整数也是一个对象。

如果要检查有效性,请尝试传递一个整数作为第一个参数,然后传递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");
}
}

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

当您使用varargs时,该方法不会期望一个数组,但是相同类型的不同参数可以以索引方式访问。

答案 2 :(得分:3)

在这种情况下

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

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

m(1)满足15.12.2.3. Phase 2: Identify Matching Arity Methods Applicable by Method Invocation Conversion

m(2)满足15.12.2.2. Phase 1: Identify Matching Arity Methods Applicable by Subtyping

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

在var arg case中

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

m(1, str1, str2);

m(3)和m(4)都满足15.12.2.4. Phase 3: Identify Applicable Variable Arity Methods。两者都没有比另一个更具体,因此含糊不清。

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

  1. 适用于子类型
  2. 适用于方法调用转换
  3. vararg,适用于子类型
  4. vararg,适用于方法调用转换
  5. 规范合并了第3组和第4组,并在第3阶段对它们进行处理。因此不一致。

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

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

答案 3 :(得分:0)

首先,这只是一些第一线索......可以编辑更多。

编译器始终搜索并选择最具体的方法。尽管阅读有点笨拙,但它在JLS 15.12.2.5中都有所指定。因此,通过调用

  

converter.pack(1,&#34; Test&#34;,&#34; Test2&#34;)

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

不同之处在于参数的数量和类型。考虑new String[]{"Test", "Test2"}是一个数组,而"Test", "Test2"是String类型的两个参数!

  

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

     

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

     

converter.pack(1,new String [] {}); //调用方法2,编译器警告

     

converter.pack(1,new Object()); //不明确,编译错误

     

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