Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Method overload ambiguity with Java 8 ternary conditional and unboxed primitives

Tags:

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?

like image 447
Robert Cooper Avatar asked May 08 '15 18:05

Robert Cooper


People also ask

What is ambiguity in method overloading in Java?

There are ambiguities while using variable arguments in Java. This happens because two methods can definitely be valid enough to be called by data values. Due to this, the compiler doesn't have the knowledge as to which method to call.

How do the overloading methods can be ambiguous?

Sometimes unexpected errors can result when overloading a method that takes a variable length argument. These errors involve ambiguity because both the methods are valid candidates for invocation. The compiler cannot decide onto which method to bind the method call.

Can you overload methods with different return types?

No, you cannot overload a method based on different return type but same argument type and number in java.


1 Answers

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.

like image 140
medvedev1088 Avatar answered Nov 01 '22 04:11

medvedev1088