35

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

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

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

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

4

6 回答 6

36

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

第一阶段(第 15.12.2.2 节)执行重载决议,不允许装箱或拆箱转换,或使用变量 arity 方法调用。如果在此阶段没有找到适用的方法,则处理继续到第二阶段。

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

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

为什么在第二阶段,编译器不选择第二种方法,因为不需要对布尔参数进行自动装箱?因为在找到这两种匹配方法后,仅使用子类型转换来确定这两种方法中最具体的方法,而不管最初为匹配它们而发生的任何装箱或拆箱(第 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 阶段,就没有选择性的自动装箱或拆箱。编译器将找到所有可访问的方法(在这些情况下都是方法)和适用的方法(同样是两种方法),然后才选择最具体的方法,而不查看装箱/拆箱,这在此处是模棱两可的。

于 2009-02-01T19:47:31.137 回答
5

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

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

于 2009-02-01T19:38:39.390 回答
3

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

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

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

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

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

请看以下案例:

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

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

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

于 2009-02-01T19:36:59.083 回答
2

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

它通常不接受第二个参数。请记住,“布尔值”也可以装箱为对象。您也可以将布尔参数显式转换为 Object 并且它会起作用。

于 2009-02-01T19:35:59.600 回答
2

请参阅http://java.sun.com/docs/books/jls/third_edition/html/expressions.html#20448

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

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

于 2009-02-01T19:46:06.037 回答
1

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 是不相关的类型,因此它们被认为是同样特定的。两种方法都不比另一种更具体,因此方法调用是模棱两可的。

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

于 2012-01-31T21:42:12.090 回答