为啥自动装箱会使 Java 中的某些调用模棱两可?

Posted

技术标签:

【中文标题】为啥自动装箱会使 Java 中的某些调用模棱两可?【英文标题】:Why does autoboxing make some calls ambiguous in Java?为什么自动装箱会使 Java 中的某些调用模棱两可? 【发布时间】:2021-11-29 13:37:40 【问题描述】:

我今天注意到自动装箱有时会导致方法重载解决方案的歧义。最简单的例子似乎是这样的:

public class Test 
    static void f(Object a, boolean b) 
    static void f(Object a, Object b) 

    static void m(int a, boolean b)  f(a,b); 

编译时出现如下错误:

Test.java:5: reference to f is ambiguous, both method
    f(java.lang.Object,boolean) in Test and method
    f(java.lang.Object,java.lang.Object) in Test match

static void m(int a, boolean b)  f(a, b); 
                                  ^

这个错误的修复很简单:只需使用显式自动装箱:

static void m(int a, boolean b)  f((Object)a, b); 

按预期正确调用第一个重载。

那么为什么重载解析失败了?为什么编译器不自动装箱第一个参数,并正常接受第二个参数?为什么我必须明确请求自动装箱?

【问题讨论】:

【参考方案1】:

当您自己将第一个参数强制转换为 Object 时,编译器将匹配该方法而不使用自动装箱 (JLS3 15.12.2):

第一阶段(§15.12.2.2)执行 未经允许的重载决议 装箱或拆箱转换,或 使用可变参数方法 调用。如果没有适用的方法 在这个阶段发现然后 处理继续到第二个 阶段。

如果你不显式地强制转换,它会进入第二阶段尝试寻找匹配方法,允许自动装箱,然后确实是模棱两可,因为你的第二个参数可以通过布尔值或对象匹配。

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

为什么在第二阶段,编译器不选择第二种方法,因为不需要对布尔参数进行自动装箱?因为在找到这两种匹配方法后,仅使用子类型转换来确定这两种方法中最具体的方法,而不管最初为匹配它们而发生的任何装箱或拆箱(第 15.12.2.5 节)。

另外:编译器不能总是根据需要的自动(取消)装箱次数来选择最具体的方法。它仍然可能导致模棱两可的情况。例如,这仍然是模棱两可的:

public class Test 
    static void f(Object a, boolean b) 
    static void f(int a, Object b) 

    static void m(int a, boolean b)  f(a, b);  // ambiguous

请记住,用于选择匹配方法的算法(编译时步骤 2)是固定的,并在 JLS 中进行了描述。一旦进入第 2 阶段,就没有选择性的自动装箱或拆箱。编译器将定位 所有 可访问的方法(这两种情况下的两种方法)和适用的方法(同样是两种方法),然后才选择最具体的方法而不查看装箱/拆箱,即这里有歧义。

【讨论】:

谢谢@eljenso。这澄清了编译器的问题,但它让我想知道为什么第二阶段是这样定义的。难道不能用“尽可能少地进行装箱/拆箱转换”来修改它吗? 好吧,谢谢@eljenso!我同意,在你的例子中,这个电话是模棱两可的。我认为这两个重载将花费相同数量的装箱转换,因此调用确实是模棱两可的。但是(不管 JLS 是什么),我认为我的示例并不模棱两可。你怎么看? 当您说您认为 f(Object, boolean) 比 f(Object, Object) 更匹配时,我可以理解您的观点。但是,JLS 在这里是明确的,并说你的电话是不明确的。您必须使用自己的语言来实现您提出的方法查找算法。 (contd.) 但那时你可能不会称它为 Java。方法重载已经是静态类型语言中最难理解/支持的部分之一。因此,与其让拳击进一步复杂化,不如避免重载或完全禁止它。 其实错误的真正原因是§15.12.2.5。从列表中选择“最具体的方法”的规则只考虑子类型转换,而不考虑在“阶段 2”期间考虑的帐户自动装箱。您可能需要更新答案。【参考方案2】:

编译器做了自动装箱第一个参数。完成后,第二个参数是模棱两可的,因为它可以被视为布尔值或对象。

This page 解释了自动装箱和选择调用哪个方法的规则。编译器首先尝试选择一个方法根本不使用任何自动装箱,因为装箱和拆箱会带来性能损失。如果在不使用装箱的情况下无法选择任何方法,就像在本例中一样,那么对于该方法的 所有 个参数,装箱就在桌面上。

【讨论】:

在这种情况下,f(Object,boolean) 不是更“具体”的方法吗? 谢谢。但是为什么它不尝试执行尽可能少的装箱/拆箱转换呢?为什么是全部还是没有? @Hosam:很好的问题,但我不知道确切的答案。可能是因为它在计算上太复杂而无法找到转换最少的调用。对于编译器而言,将装箱应用于所有参数或不应用于任何参数是一种更简单的实现。 “因为装箱和拆箱会带来性能损失。”没错,但不是这里的问题。阶段 1 是为了保证与 pre-JDK5 的向后兼容性。 @Bill,我不认为计算太复杂。只需计算每个候选方法的转换次数,对它们进行排序,然后选择最好的方法(除非前两种方法相等)。这对我来说似乎并不复杂!【参考方案3】:

当你说 f(a, b) 时,编译器会混淆它应该引用哪个函数。

这是因为 a 是一个 int,但 f 中预期的参数是一个 Object。所以编译器决定将 a 转换为 Object。现在的问题是,如果 a 可以转换为对象,那么 b 也可以。

这意味着函数调用可以引用任一定义。这使得调用模棱两可。

当您手动将 a 转换为 Object 时,编译器只会查找最接近的匹配项,然后引用它。

为什么编译器没有选择 可以通过“做”达到的功能 尽可能少的数量 装箱/拆箱转换”?

看下面的案例:

f(boolean a, Object b)
f(Object a , boolean b)

如果我们像f(boolean a, boolean b)这样调用,应该选择哪个函数?模棱两可吧?同样,当存在大量参数时,这将变得更加复杂。所以编译器选择给你一个警告。

由于无法知道程序员真正打算调用哪一个函数,因此编译器会报错。

【讨论】:

谢谢@Niyaz。但即使 a 和 b 可以转换为对象,它也没有必要。因此,编译器(恕我直言)必须将 a 转换为对象,但对于 b 它应该选择最具体的重载,即第一个重载。这个逻辑有什么问题? 如果给定示例中有更多参数,编译器如何选择“最具体的重载”?这就是问题所在。 尼亚兹。如果他投射到 Object 有什么区别?这两个函数都接受对象。所以我会本能地说它没有更好的匹配。 正如我所说,当编译器尝试自己匹配时:如果 A 可以转换为对象,那么 B 也可以。所以这两个函数中的任何一个都是可能的。当我们手动将 A 转换为 Object 时(即使在两个函数中 A 都是 Object),编译器不再需要任何类型的转换。它只是匹配。 但它不能尝试“执行尽可能少的装箱/拆箱转换”吗?这会导致最具体的过载,不是吗? (当然,如果两个或多个方法的最小可能转换次数相关,则调用是不明确的。)【参考方案4】:

那么为什么重载决议 失败?为什么编译器没有自动装箱 第一个参数,并接受 第二个参数通常?为什么我 必须请求自动装箱 明确的?

它通常不接受第二个参数。请记住,“布尔值”也可以装箱为对象。您也可以将布尔参数显式转换为 Object,这样就可以了。

【讨论】:

谢谢@Kevin。是的,我可以明确地将它装箱,但我没有。那么为什么它不选择最具体的重载,在这种情况下是第一个呢? 因为编译器没有足够的信息来为您做出决定。 两个方法都应用了,因为布尔值可以装箱到一个对象 我认为这不能回答问题。当应用多种方法时,会自动选择需要最少扩展转换的方法。只有当多个方法需要相同数量的“扩展”时,才会出现歧义错误。我的猜测是拳击不算在扩大范围内。【参考方案5】:

见http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#20448

演员表有帮助,因为这样就不需要装箱来找到要调用的方法。没有演员表,第二次尝试是允许装箱,然后布尔值也可以装箱。

最好有清晰易懂的规格说明会发生什么,而不是让人们猜测。

【讨论】:

所以,这就像“一旦自动装箱,我们也可以使用其他参数”和“如果还没有自动装箱发生,我们也尽量不自动装箱”? 谢谢@iny。对于今天没有投票,我深表歉意! @litb,是的,显然是这样。但我想知道为什么他们不尝试“执行尽可能少的装箱/拆箱转换”。【参考方案6】:

Java 编译器分阶段解析重载的方法和构造函数。在第一阶段 [§15.12.2.2],它通过子类型 [§4.10] 确定适用的方法。在这个例子中,这两种方法都不适用,因为 int 不是 Object 的子类型。

在第二阶段 [§15.12.2.3],编译器通过方法调用转换 [§5.3] 识别适用的方法,这是自动装箱和子类型的组合。对于这两种重载,int 参数都可以转换为 Integer,它是 Object 的子类型。 boolean 参数对于第一个重载不需要转换,并且可以转换为 Boolean,Object 的子类型,对于第二个。因此,这两种方法都适用于第二阶段。

由于不止一种方法适用,编译器必须确定哪种方法最具体 [§15.12.2.5]。它比较参数类型,而不是参数类型,并且不会自动装箱。 Object 和 boolean 是不相关的类型,因此它们被认为是同样特定的。两种方法都不比另一种更具体,因此方法调用不明确。

解决歧义的一种方法是将布尔参数更改为布尔类型,它是对象的子类型。第一个重载总是比第二个更具体(如果适用)。

【讨论】:

以上是关于为啥自动装箱会使 Java 中的某些调用模棱两可?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我们在 Java 中使用自动装箱和拆箱?

为啥初始化列表中的元素数量会导致模棱两可的调用错误?

JAVA——装箱和拆箱

Java中的自动装箱与拆箱

深入剖析Java中的装箱和拆箱

为啥这些重载的函数调用模棱两可?