为什么此方法重载模棱两可?


问题内容

public class Primitive {
void m(Number b, Number … a) {} // widening, autoboxing->widening->varargs

    void m(byte b, Number ... a) {} // unboxing, autoboxing->widening->varargs

    public static void main(String[] args) {
        Byte b = 12;
        Primitive obj = new Primitive();
        obj.m(b, 23);
    }
}

我已经搜索过,发现加宽优先级比拆箱优先,因此在上述方法调用中,应该调用第一个方法,因为两个参数都相同。但这不会发生。你能解释一下吗?


问题答案:

它无法在JDK 1.5、1.6和1.7中进行编译,但可以在JDK 1.8中工作。

更新 :这似乎是一个事实,即它与第一JDK8版本的工作实际上是一个错误:它曾在JDK
1.8.0_05,但根据这个问题,并通过medvedev1088答案,这个代码将 不会 再在1.8.0_25编译,这是符合JLS的行为

我认为这不是已修复的错误。相反,它是与Java 8中lambda表达式的方法调用机制相关的更改的影响。

大多数人可能会同意,关于“方法调用表达式”的部分是迄今为止Java语言规范中最复杂,最难以理解的部分。可能会有整个工程师团队负责交叉检查和验证此部分。因此,任何陈述或任何试图进行的推理都应花费大量的盐。(即使来自上述工程师)。但我会尝试一下,至少充实其他人可能会参考以进行进一步分析的相关部分:

考虑有关

并且考虑到这两种方法都是“潜在适用方法”(JLS7
/
JLS8),则相关的小节是关于

对于JLS 7,它指出

当且仅当满足以下所有条件时,方法m是 适用的可变对数方法

  • 对于1 = i <n,可以通过方法调用转换将ei的类型Ai转换为Si。

(其他条件指的是此处不相关的调用形式,例如,真正 使用 varargs的调用或涉及泛型的调用)

参见示例:当可以通过方法调用转换将其转换为相应的形式方法参数时,方法适用于b类型的实际参数表达式。根据有关JLS7中方法调用转换的相应部分,允许进行以下转换:Byte``b

  • 身份转换(第5.1.1节)
  • 不断扩大的原始转换(第5.1.2节)
  • 扩展参考转换(第5.1.5节)
  • 装箱转换(第5.1.7节),然后可选地扩大参考转换
  • 取消装箱转换(第5.1.8节),然后可以选择加宽原始转换。

显然,根据此规范,可以采用 两种 方法:

  • m(Number b, Number ... a) 通过扩展参考转换适用
  • m(byte b, Number ... a) 通过拆箱转换适用

您提到您 “ …发现扩展优先级高于拆箱” ,但这在此处不适用:上面列出的条件不涉及任何“优先级”。它们被列为不同的选项。即使第一种方法是void m(Byte b, Number ... a)适用的,“身份转换”也将适用,但是它仍然仅算作 一种 可能的转换,并且由于含糊不清而导致错误方法。


因此,据我所知,这解释了为什么它 不适 用于JDK7。我没有详细弄清楚它为什么 没有 工作,JDK8。但是,在JLS
8中,可变可变方法的适用性
的定义在“可变可变调用的适用方法的识别”中发生了细微变化

如果m不是通用方法,则对于1≤i≤k,或者ei在宽松的调用上下文中与Ti兼容,或者ei与适用性无关(第15.12.2.2节),则m可通过可变arity调用来应用。

(我尚未深入研究“松散调用上下文”和第15.12.2.2节的定义,但这似乎是关键的区别)


顺便说一句,再次提到您 “ …发现扩展优先级比拆箱优先” 的说法:对于
涉及varargs(并且根本不需要方法调用转换)的方法,这是正确的。如果您在示例中遗漏了变量,那么查找匹配方法的过程将从阶段1:确定适用于子类型的匹配Arity方法开始。该方法由于是的子类型,因此m(Number b)已经适用于该参数。没有理由进入第2阶段:确定适用于方法调用转换的匹配Arity方法。在此阶段,通过从以下位置取消装箱来进行方法调用转换Byte b``Byte``NumberBytebyte将适用,但从来没有达到这个阶段。