Method overload ambiguity with Java 8 ternary cond

2019-01-22 18:45发布

The following is code compiles in Java 7, but not 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
}

The compiler claims the last method call ambiguous.

If we change the type of the second parameter of f from int to Integer, then the code compiles on both platforms. Why doesn't the posted code compile in Java 8?

2条回答
beautiful°
2楼-- · 2019-01-22 19:08

Let's first consider a simplified version that doesn't have a ternary conditional and doesn't compile on Java HotSpot VM (build 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
    }
}

The compiler error is:

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

According to 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).

Invocation has to do with invocation context which is explained here JLS 5.3. Invocation Contexts

When no boxing or unboxing is involved for a method invocation then strict invocation applies. When boxing or unboxing is involved for a method invocation then loose invocation applies.

Identifying applicable methods is divided into 3 phases.

The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the second phase.

The second phase (§15.12.2.3) performs overload resolution while allowing boxing and unboxing, but still precludes the use of variable arity method invocation. If no applicable method is found during this phase then processing continues to the third phase.

The third phase (§15.12.2.4) allows overloading to be combined with variable arity methods, boxing, and unboxing.

For our case there are no methods applicable by strict invocation. Both methods are applicable by loose invocation since the double value has to be boxed.

According to 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.

Then:

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).

It may look that the 2nd condition matches for this case but in fact it doesn't because int is not a subtype of Object: it's not true that int <: Object. However if we replace int with Integer in the f method signature this condition would match. Note that the 1st parameter in methods matches this condition since Object <: Object is true.

According to $4.10 no subtype/supertype relation is defined between primitive types and Class/Interface types. So int is not a subtype of Object for example. Thus int is not more specific than Object.

Since among the 2 methods there are no more specific methods thus there can be no strictly more specific and can be no most specific method (the JLS gives definitions for those terms in the same paragraph JLS 15.12.2.5 Choosing the Most Specific Method). So both methods are maximally specific.

In this case the JLS gives 2 options:

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

This is not our case, thus

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

The compile-time error for our case looks valid according to the JLS.

What happens if we change method parameter type from int to Integer?

In this case both methods are still applicable by loose invocation. However the method with Integer parameter is more specific than the method with 2 Object parameters since Integer <: Object. The method with Integer parameter is strictly more specific and most specific thus the compiler will choose it and not throw a compile error.

What happens if we change double to Double in this line: double d = 1.0;?

In this case there is exactly 1 method applicable by strict invocation: no boxing or unboxing is required for invocation of this method: f(Object o1, int i). For the other method you need to do boxing of int value so it's applicable by loose invocation. The compiler can choose the method applicable by strict invocation thus no compiler error is thrown.

As Marco13 pointed out in his comment there is a similar case discussed in this post Why is this method overloading ambiguous?

As explained in the answer there were some major changes related to the method invocation mechanisms between Java 7 and Java 8. This explains why the code compiles in Java 7 but not in Java 8.


Now comes the fun part!

Let's add a ternary conditional operator:

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

The compiler complains about ambiguous method invocation. The JLS 15.12.2 doesn't dictate any special rules related to ternary conditional operators when performing method invocations.

However there are JLS 15.25 Conditional Operator ? : and JLS 15.25.3. Reference Conditional Expressions. The former one categorizes conditional expressions into 3 subcategories: boolean, numeric and reference conditional expression. The second and third operands of our conditional expression have types String and double respectively. According to the JLS our conditional expression is a reference conditional expression.

Then according to JLS 15.25.3. Reference Conditional Expressions our conditional expression is a poly reference conditional expression since it appears in an invocation context. The type of our poly conditional expression thus is Object (the target type in the invocation context). From here we could continue the steps as if the first parameter is Object in which case the compiler should choose the method with int as the second parameter (and not throw the compiler error).

The tricky part is this note from JLS:

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

From this we can assume (also the "poly" in the name implies this) that in the context of method invocation the 2 operands should be considered independently. What this means is that when the compiler has to decide whether a boxing operation is required for such argument it should look into each of the operands and see if a boxing may be required. For our specific case String doesn't require boxing and double will require boxing. Thus the compiler decides that for both overloaded methods it should be a loose method invocation. Further steps are the same as in the case when instead of a ternary conditional expression we use a double value.

From the explanation above it seems that the JLS itself is vague and ambiguous in the part related to conditional expressions when applied to overloaded methods so we had to make some assumptions.

What's interesting is that my IDE (IntelliJ IDEA) doesn't detect the last case (with the ternary conditional expression) as a compiler error. All other cases it detects according to the java compiler from JDK. This means that either JDK java compiler or the internal IDE parser has a bug.

查看更多
该账号已被封号
3楼-- · 2019-01-22 19:25

In short:

The compiler doesn't know which method to choose since the ordering between primitive and reference types is not defined in JLS in regards to choosing most specific method.

When you use Integer instead of int the compiler picks the method with Integer because Integer is a subtype of Object.

When you use Double instead of double the compiler picks the method that doesn't involve boxing or unboxing.

Prior to Java 8 the rules were different so this code could compile.

查看更多
登录 后发表回答