Java:泛型方法重载的歧义性

34

Consider the following code:

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

上述代码可以编译通过。但是,如果将pack签名中的String[]改为String...,并将new String[]{"Test", "Test2"}改为"Test", "Test2",编译器会抱怨对converter.pack的调用存在歧义。
我能理解为什么这可能被认为是有歧义的(因为int可以自动装箱为Integer,从而符合条件,或者缺乏条件,即K)。然而,我无法理解的是,如果使用String[]而不是String...,为什么就没有歧义。
请问有人能够解释这种奇怪的行为吗?

1
以前也有类似的问题,但我承认它们有点特殊,难以找到。尽管如此,这仍然让人思考!:S - Peter Jaloveczki
2
无论是否有语法糖,我仍然期望它与语言的其余部分一致地表现。 - BambooleanLogic
3
这确实是一个不错的问题,很难得到确切的答案。 - Rohit Jain
有一件事是我们通常不应该重载var args方法。编译器可能会变得非常混乱。当我们使用String[]而不是String...时,就没有var args方法,所以你可能没有遇到任何问题。但我仍在努力找出编译错误的原因;) - Shiva Kumar
@stonedsquirrel。嗯,看起来你是对的。这让我感到惊讶。我本来期望编译器可以从方法调用上下文中推断出K的类型。因此,正如赋值所示,K应该被推断为IntWrapper - Rohit Jain
显示剩余5条评论
4个回答

15
您的第一个案例非常直接。以下是方法:
public MyContainer<IntWrapper> pack(int key, Object[] values) 

参数完全匹配 - (1, String[])。根据JLS第15.12.2节

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

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


编译器似乎在您的第二种情况中发挥了技巧:

在您的第二个案例中,由于使用了var-args,因此将使用var-args和装箱或拆箱来执行方法重载解析,根据该JLS部分中解释的第3阶段:

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

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

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

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

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

编译器应该从左侧推断出泛型方法中K的类型为IntWrapper,但它似乎将K推断为Integer类型,因此现在对于这个方法调用,两种方法都同样适用,因为它们都需要var-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,其中介绍了编译器如何推断类型参数,但那超出了我的理解范围。

因此,目前你只能忍受这种情况,并使用替代方法(提供显式类型参数)。


事实证明,编译器并没有耍什么花招:

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

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


为什么在使用var args时,第一阶段(15.12.2.2)无法解析。我看到的是,在没有装箱的情况下,它本可以识别正确的签名。 - Shiva Kumar
@LastFreeNickname。是的,编译器无法推断类型,当结果未分配给LHS时,但在这种情况下应该可以。也许为了一致性,此调用被标记为模糊不清。 - Rohit Jain
所以这似乎是一个设计决策。干得好。你应该添加第二阶段是“允许装箱和拆箱,但仍然排除使用可变元方法调用”。你觉得在当前的第2和第3个阶段之间添加一个分析可变元方法调用但忽略装箱的阶段是可能的吗?他们只是没有这样做。 - André Stannek
1
15.12.2.8 仅适用于未从 15.12.2.7 推断出的类型参数。在这种情况下,K 已经从方法参数 1 推断为 Integer,因此赋值上下文不用于推断。 - ZhongYu
@zhong.j.yu。啊,我太粗心了。感谢您抽出时间浏览这些部分。 :) - Rohit Jain
显示剩余4条评论

3
在这种情况下
(1) m(K,   String[])
(2) m(int, String[])

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

m(1)符合15.12.2.3。第二阶段:通过方法调用转换标识匹配的数量方法

m(2)符合15.12.2.2. 第一阶段:通过子类型标识匹配的数量方法

编译器在第一阶段停止;它找到m(2)作为该阶段唯一适用的方法,因此选择m(2)。

在可变参数的情况下

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

m(1, str1, str2);

m(3)和m(4)都符合15.12.2.4. 第三阶段: 确定可应用的变量参数方法。两者都不比另一个更具体,因此存在歧义。

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

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

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

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

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


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

第二种方法和第一种方法同样好。
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");
}
}

此外,请不要混淆数组和可变参数,它们有着完全不同的用途。
当您使用可变参数时,方法并不期望一个数组,而是相同类型的不同参数,可以按索引方式访问。

9
嗯...我是唯一一个没听懂的吗? - Georgian
7
你是在说编译器把“int,Object...”和“Object...”视为等价吗?如果这确实是真的,那对我来说听起来完全荒谬。 - BambooleanLogic
@GGrec:我支持这个提议,但我不明白。 - Peter Jaloveczki
这是荒谬的。int,Object...告诉我们必须至少有一个int参数,而Object...可以是零个或多个Object类型。 - Georgian
我不确定这是否与我的现实场景相关。但它可能与我提出的简化问题相关。我将稍微更新我的示例,以查看是否仍然适用。 - BambooleanLogic
显示剩余7条评论

0

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

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

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

编译器无法确定1应该解析为K还是int。换句话说,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,无警告

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接