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...pack签名和new String[]{"Test", "Test2"}"Test", "Test2"编译器抱怨converter.pack含糊不清

现在,我可以理解为什么它可能被认为是模糊的(因为int可以被自动装入一个Integer ,从而与K的条件相匹配或缺less)。 但是,我不明白的是,如果你使用String[]而不是String... ,那么歧义就不存在了。

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

你的第一个案子非常简单。 以下方法:

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

与参数完全匹配 – (1, String[]) 。 从JLS第15.12.2节 :

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

现在,在将这些parameter passing给第二种方法时没有涉及到拳击。 由于Object[]String[]的超级types。 在Java 5之前,传递Object[]参数的String[]参数是一个有效的调用。


编译器似乎玩你的第二个案例:

在第二种情况下,由于您已经使用了var-args,所以重载parsing的方法将使用var-args和boxing或unboxing完成,按照JLS部分中的第三个阶段:

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

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

第二阶段(§15.12.2.3)在允许装箱和拆箱的同时执行重载parsing,但是仍然排除了使用variables方法调用。

现在在这里发生了什么是编译器不正确推断types参数* (实际上,它正确推断它为types参数被用作forms参数,看到更新结束这个答案)。 所以,对于你的方法调用:

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

编译器应该从LHS中推断出generics方法中的Ktypes是IntWrapper 。 但是似乎推断K是一个Integertypes,由于这两个方法现在同样适用于这个方法调用,因为它们都需要var-argsboxing

但是,如果这个方法的结果没有被赋值给某个引用,那么我可以理解,编译器不能在这种情况下推断出正确的types,哪里是完全可以接受的歧义错误:

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

可能是我想,只是为了保持一致性,这也是第一种情况的含糊不清。 但是,我也不太确定,因为我没有findJLS的可信来源,或者其他官方提到的这个问题。 我会继续search,如果我find一个,将更新答案。


让我们通过显式的types信息欺骗编译器:

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

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

现在,typesK将被推断为IntWrapper ,但由于1不能转换为IntWrapper ,所以该方法被丢弃,并且第二个方法将被调用并且它将工作得很好。


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

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

但是,在这种情况下是不行的。 所以这可能是一个错误。

*或者可能是我不明白编译器如何推断types参数,当types不作为parameter passing。 所以,为了更多地了解这个,我尝试了一下–JLS第15.12.2.7节和JLS第15.12.2.8节 ,它是关于编译器如何推断types参数的,但是这完全在我头顶。

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


事实certificate,编译器并没有玩什么窍门:

正如@ zhong.j.yu所说的最后解释,编译器只对types推断使用15.12.2.8节,当它不能按照15.12.2.7节推断时。 但是在这里,它可以从传入的参数中推断出types为Integer ,因为types参数显然是方法中的格式参数。

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

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

这就是为什么你得到一个模糊不清

编辑是的我想说,他们是相同的编译。 使用可变参数的目的是让用户在不确定给定types的参数个数的时候定义一个方法。

所以如果你使用一个对象作为可变参数,你只需要说编译器,我不知道我将发送多less个对象,另一方面,你说“我传递一个整数和未知数目的对象”。 对于编译器来说,整数也是一个对象。

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

另外,请不要使用数组和variables参数,它们有一些不同的用途。

当你使用可变参数时,这个方法并不期望一个数组,但是可以以索引的方式访问同一types的不同参数。

在这种情况下

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

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

m(2)满足15.12.2.2。 第一阶段:确定子types适用的匹配方法

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

在var arg情况下

 (3) m(K, String...) (4) m(int, String...) m(1, str1, str2); 

m(3)和m(4)满足15.12.2.4。 阶段3:确定适用的可变参数方法 。 没有比其他更具体,因此模糊。

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

  1. 适用于分类
  2. 适用于方法调用转换
  3. 可变分值,适用于分类
  4. vararg,通过方法调用转换适用

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

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

另一个批评是,不应该有所有这些阶段,因为程序员不这样想。 我们应该简单地find所有适用的方法,然后select最具体的方法(有一些机制,以避免拳击/拆箱)

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

编译器总是search并select最具体的方法。 尽pipe阅读起来有点笨拙,但都是在JLS 15.12.2.5中指定的。 因此,通过调用

converter.pack(1,“Test”,“Test2”)

编译器不能确定是否将1解散为Kint 。 换句话说,K可以应用于任何types,所以它与int / Integer处于同一级别。

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

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,没有警告