Java 8 个三元条件和未装箱原语的方法重载歧义

Method overload ambiguity with Java 8 ternary conditional and unboxed primitives

以下是Java7编译出来的代码,不是openjdk-1.8.0.45-31.b13.fc21.

static void f(Object o1, int i) {}
static void f(Object o1, Object o2) {}

static void test(boolean b) {
    String s = "string";
    double d = 1.0;
    // The supremum of types 'String' and 'double' is 'Object'
    Object o = b ? s : d;
    Double boxedDouble = d;
    int i = 1;
    f(o,                   i); // fine
    f(b ? s : boxedDouble, i); // fine
    f(b ? s : d,           i); // ERROR!  Ambiguous
}

编译器声称最后一个方法调用不明确。

如果我们将 f 的第二个参数的类型从 int 更改为 Integer,则代码可以在两个平台上编译。为什么发布的代码不能在 Java 8 中编译?

让我们首先考虑一个没有三元条件且不在 Java HotSpot VM(构建 1.8.0_25-b17)上编译的简化版本:

public class Test {

    void f(Object o1, int i) {}
    void f(Object o1, Object o2) {}

    void test() {
        double d = 1.0;

        int i = 1;
        f(d, i); // ERROR!  Ambiguous
    }
}

编译错误为:

Error:(12, 9) java: reference to f is ambiguous
both method f(java.lang.Object,int) in test.Test and method f(java.lang.Object,java.lang.Object) in test.Test match

根据JLS 15.12.2. Compile-Time Step 2: Determine Method Signature

A method is applicable if it is applicable by one of strict invocation (§15.12.2.2), loose invocation (§15.12.2.3), or variable arity invocation (§15.12.2.4).

调用与调用上下文相关,此处有解释 JLS 5.3. Invocation Contexts

当方法调用不涉及装箱或拆箱时,将应用严格调用。当方法调用涉及装箱或拆箱时,松散调用适用。

确定适用的方法分为三个阶段。

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

第二阶段 (§15.12.2.3) 执行重载决议,同时允许装箱和拆箱,但仍然排除使用变量 arity 方法调用。 如果在此阶段没有找到适用的方法,则处理继续到第三阶段。

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

对于我们的案例,没有可通过严格调用应用的方法。这两种方法都适用于松散调用,因为双精度值必须装箱。

根据JLS 15.12.2.5 Choosing the Most Specific Method

If more than one member method is both accessible and applicable to a method invocation, it is necessary to choose one to provide the descriptor for the run-time method dispatch. The Java programming language uses the rule that the most specific method is chosen.

然后:

One applicable method m1 is more specific than another applicable method m2, for an invocation with argument expressions e1, ..., ek, if any of the following are true:

  1. m2 is generic, and m1 is inferred to be more specific than m2 for argument expressions e1, ..., ek by §18.5.4.

  2. m2 is not generic, and m1 and m2 are applicable by strict or loose invocation, and where m1 has formal parameter types S1, ..., Sn and m2 has formal parameter types T1, ..., Tn, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ n, n = k).

  3. m2 is not generic, and m1 and m2 are applicable by variable arity invocation, and where the first k variable arity parameter types of m1 are S1, ..., Sk and the first k variable arity parameter types of m2 are T1, ..., Tk, the type Si is more specific than Ti for argument ei for all i (1 ≤ i ≤ k). Additionally, if m2 has k+1 parameters, then the k+1'th variable arity parameter type of m1 is a subtype of the k+1'th variable arity parameter type of m2.

The above conditions are the only circumstances under which one method may be more specific than another.

A type S is more specific than a type T for any expression if S <: T (§4.10).

在这种情况下,第二个条件可能看起来匹配,但实际上并不匹配,因为 int 不是 Object 的子类型:int <: 对象。但是,如果我们在 f 方法签名中将 int 替换为 Integer,则此条件将匹配。请注意,方法中的第一个参数匹配此条件,因为 Object <: Object 为真。

根据.10,基本类型和Class/Interface类型之间没有定义subtype/supertype关系。例如,int 不是 Object 的子类型。因此 int 并不比 Object.

更具体

因为在这两种方法中没有更具体的方法因此不可能有更具体并且不能 最具体的 方法(JLS 在同一段落中给出了这些术语的定义 JLS 15.12.2.5 Choosing the Most Specific Method)。所以这两种方法都是最具体

在这种情况下,JLS 给出了 2 个选项:

If all the maximally specific methods have override-equivalent signatures (§8.4.2) ...

这不是我们的情况,因此

Otherwise, the method invocation is ambiguous, and a compile-time error occurs.

根据 JLS,我们案例的编译时错误看起来是有效的。

如果我们将方法参数类型从 int 更改为 Integer 会发生什么?

在这种情况下,这两种方法仍然适用于松散调用。然而,具有 Integer 参数的方法比具有 2 个 Object 参数的方法更具体,因为 Integer <: Object。带有 Integer 参数的方法更具体和最具体,因此编译器将选择它并且不会抛出编译错误。

如果我们在这一行中将 double 更改为 Double 会发生什么:double d = 1.0;?

在这种情况下,只有 1 个方法适用于严格调用:调用此方法不需要装箱或拆箱:f(Object o1, int i)。对于另一种方法,您需要对 int 值进行装箱,以便通过松散调用应用它。编译器可以通过严格调用来选择适用的方法,因此不会抛出编译器错误。

正如 Marco13 在他的评论中指出的,此 post Why is this method overloading ambiguous?

中讨论了一个类似的案例

如答案中所述,Java 7 和 Java 8 之间的方法调用机制发生了一些重大变化。这解释了为什么代码在 Java 7 中编译但是不在 Java 8.


有趣的部分来了!

让我们添加一个三元条件运算符:

public class Test {

    void f(Object o1, int i) {
        System.out.println("1");
    }
    void f(Object o1, Object o2) {
        System.out.println("2");
    }

    void test(boolean b) {
        String s = "string";
        double d = 1.0;
        int i = 1;

        f(b ? s : d, i); // ERROR!  Ambiguous
    }

    public static void main(String[] args) {
        new Test().test(true);
    }
}

编译器抱怨方法调用不明确。 在执行方法调用时,JLS 15.12.2 不规定任何与三元条件运算符相关的特殊规则。

不过有JLS 15.25 Conditional Operator ? : and JLS 15.25.3. Reference Conditional Expressions。前者将条件表达式分为 3 个子类:布尔、数值和引用条件表达式。条件表达式的第二个和第三个操作数分别具有 String 和 double 类型。根据JLS我们的条件表达式是引用条件表达式。

然后根据 JLS 15.25.3. Reference Conditional Expressions 我们的条件表达式是一个多引用条件表达式,因为它出现在调用上下文中。因此,我们的多条件表达式的类型是 Object(调用上下文中的目标类型)。从这里我们可以继续这些步骤,就好像第一个参数是 Object 在这种情况下,编译器应该选择使用 int 作为第二个参数的方法(而不是抛出编译器错误)。

棘手的部分是来自 JLS 的注释:

its second and third operand expressions similarly appear in a context of the same kind with target type T.

由此我们可以假设(名称中的 "poly" 也暗示了这一点)在方法调用的上下文中,应独立考虑 2 个操作数。这意味着当编译器必须决定是否需要对此类参数进行装箱操作时,它应该查看每个操作数并查看是否需要装箱。对于我们的特定情况,String 不需要装箱,double 需要装箱。因此,编译器决定对于这两个重载方法,它应该是一个松散的方法调用。进一步的步骤与我们使用双精度值而不是三元条件表达式的情况相同。

从上面的解释看来,JLS本身在应用于重载方法时,在条件表达式相关的部分是含糊不清的,所以我们不得不做一些假设。

有趣的是,我的 IDE (IntelliJ IDEA) 没有将最后一种情况(使用三元条件表达式)检测为编译器错误。它根据来自 JDK 的 java 编译器检测到的所有其他情况。这意味着 JDK java 编译器或内部 IDE 解析器有错误。

简而言之:

编译器不知道选择哪种方法,因为在选择最具体的方法方面,JLS 中未定义基本类型和引用类型之间的顺序。

当您使用 Integer 而不是 int 时,编译器会选择使用 Integer 的方法,因为 Integer 是 Object 的子类型。

当您使用 Double 而不是 double 时,编译器会选择不涉及装箱或拆箱的方法。

在 Java8 之前,规则不同,因此此代码可以编译。