Java:泛型方法重载歧义

Posted

技术标签:

【中文标题】Java:泛型方法重载歧义【英文标题】:Java: Generic method overloading ambiguity 【发布时间】:2013-08-29 21:27:07 【问题描述】:

考虑以下代码:

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

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

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

【问题讨论】:

以前也有类似的问题,但我承认它们有点特别,很难找到。尽管如此,还是让人联想到! : S 这个语法糖时不时让你发痒,你所能做的就是阅读 JLS 并等待答案! 语法糖与否,我仍然希望它与语言的其余部分保持一致。 这真是一个很好的问题。很难得到真正的答案。 最令人惊讶的行为是第一个方法根本不适用于该方法调用,因为1 不能通过方法调用转换转换为IntWrapper。这真的在吃我的头。仍在 JLS 周围漫游。 【参考方案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 之前,为 Object[] 参数传递 String[] 参数也是有效的调用。


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

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

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

注意,这里不适用第二阶段,因为使用了var-args

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

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

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

编译器应该从 LHS 推断出泛型方法中 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.7 和 JLS §15.12.2.8,这是关于编译器如何推断类型参数的,但这完全超出了我的想象。

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


事实证明,Compiler 并没有耍什么花招:

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

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

【讨论】:

为什么第一阶段(15.12.2.2)在使用var args时无法解析。我看到的是,如果没有装箱,它可以识别出正确的签名。 好的,带来更多亮点。尽管如此,我不同意编译器可以甚至应该从 LHS 中扣除要调用的方法(您的第二段)。在我的回答中,我调用了这些方法,而只是忽略了结果。 @LastFreeNickname。是的,当结果未分配给 LHS 时,编译器无法推断类型是正确的,但在这种情况下应该如此。可能出于一致性目的,此调用被标记为模棱两可。 @ShivaKumar。如果您阅读了 JLS 部分,您会知道 var-args 是最后考虑的内容。 15.12.2.8 仅适用于不是从 15.12.2.7 推断的类型参数。在这种情况下,K 已经从方法参数1 推断为 Integer,因此不使用赋值上下文进行推断。【参考方案2】:

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

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

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

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

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

如果您想检查有效性,请尝试将整数作为第一个参数传递,然后传递 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,它们有一些完全不同的用途。

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

【讨论】:

呃……只有我一个没有关注吗? 你是说编译器将“int, Object...”和“Object...”视为等价的吗?如果这确实是真的,我觉得这完全是荒谬的。 @GGrec:我会支持那个我不明白的动议。 这太荒谬了。 int, Object... 告诉我们必须至少有一个int 参数,而Object... 可以是一个或多个Object 类型。 其实经过快速测试,确实是这样的:public static void a(int x, Object... y) public static void a(Object... y) public static void main(String[] args) a(1, "", ""); 【参考方案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 的情况下

(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 组:

    可通过子类型应用 适用于方法调用转换 可变参数,通过子类型应用 可变参数,通过方法调用转换适用

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

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

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

【讨论】:

【参考方案4】:

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

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

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

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

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

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

【讨论】:

以上是关于Java:泛型方法重载歧义的主要内容,如果未能解决你的问题,请参考以下文章

方法重载和泛型

Java协变式覆盖(Override)和泛型重载(Overload)

您可以使用泛型进行方法重载并且只更改方法签名的泛型类型吗?

Java重载遇到泛型

c#泛型方法重载

无法用类型约束重载泛型方法[重复]