简体   繁体   中英

Java: Generic method overloading ambiguity

Consider the following code:

public class Converter {

    public <K> MyContainer<K> pack(K key, String[] values) {
        return new MyContainer<>(key);
    }

    public MyContainer<IntWrapper> pack(int key, String[] values) {
        return new MyContainer<>(new IntWrapper(key));
    }


    public static final class MyContainer<T> {
        public MyContainer(T object) { }
    }

    public static final class IntWrapper {
        public IntWrapper(int i) { }
    }


    public static void main(String[] args) {
        Converter converter = new Converter();
        MyContainer<IntWrapper> test = converter.pack(1, new String[]{"Test", "Test2"});
    }
}

The above code compiles without problems. However, if one changes String[] to String... in both pack signatures and new String[]{"Test", "Test2"} to "Test", "Test2" , the compiler complains about the call to converter.pack being ambiguous.

Now, I can understand why it could be considered ambiguous (as int can be autoboxed into an Integer , thus matching the conditions, or lack thereof, of K ). However, what I can't understand is why the ambiguity isn't there if you're using String[] instead of String... .

Can someone please explain this odd behavior?

Your 1 st case is pretty straight-forward. The below method:

public MyContainer<IntWrapper> pack(int key, Object[] values) 

is an exact match for arguments - (1, String[]) . From JLS Section 15.12.2 :

The first phase (§15.12.2.2) performs overload resolution without permitting boxing or unboxing conversion

Now, there is no boxing involved while passing those parameters to the 2nd method. As Object[] is a super type of String[] . And passing String[] argument for Object[] parameter was a valid invocation even before Java 5.


Compiler seems to play trick in your 2nd case:

In your 2nd case, since you have used var-args, the method overloading resolution will be done using both var-args, and boxing or unboxing, as per the 3rd phase explained in that JLS section:

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

Note, the 2nd phase is not applicable here, due to the use of var-args :

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.

Now what is happening here is compiler is not inferring the type argument correctly* (Actually, it's inferring it correctly as the type parameter is used as formal parameter, see the update towards the end of this answer). So, for your method invocation:

MyContainer<IntWrapper> test = converter.pack(1, "Test", "Test2");

compiler should have inferred the type of K in generic method to be IntWrapper , from the LHS. But it seems like it is inferring K to be an Integer type, due to which both your methods are now equally applicable for this method call, as both of the requires var-args or boxing .

However, if the result of that method is not assigned to some reference, then I can understand that compiler cannot infer proper type as in this case, where is is perfectly acceptable to give an ambiguity error:

converter.pack(1, "Test", "Test2");

May be I guess, just to maintain consistency, it is also marked ambiguous for the first case. But, again I'm not really sure, as I haven't found any credible source from JLS, or other official reference which talks about this issue. I'll keep on searching, and if I get to find one, will update the answer.


Let's trick the compiler by explicit type information:

If you change the method invocation to give explicit type information:

MyContainer<IntWrapper> test = converter.<IntWrapper>pack(1, "Test", "Test2");

Now, the type K will be inferred as IntWrapper , but since 1 is not convertible to IntWrapper , that method is discarded, and 2nd method will be invoke and it will work perfectly fine.


Frankly speaking, I really don't know what is happening here. I would expect the compiler to infer the type parameter from the method invocation context in the first case also, as it works for following problem:

public static <T> HashSet<T> create(int size) {  
    return new HashSet<T>(size);  
}
// Type inferred as `Integer`, from LHS.
HashSet<Integer> hi = create(10);  

But, it is not doing in this case. So this can possibly be a bug.

*Or may be I don't understand exactly how the Compiler infers the type arguments, when the type is not passed as an argument. So, for learning more about this, I tried going through - JLS §15.12.2.7 and JLS §15.12.2.8 , which is about how compiler infers the type argument, but that is going completely over the very top of my head.

So, for now you have to live with it, and use the alternative (providing explicit type argument).


It turns out, Compiler wasn't playing any trick:

As finally explained in comment by @zhong.j.yu., compiler only applies section 15.12.2.8 for type inference, when it fails to infer it as per 15.12.2.7 section. But here, it can infer the type as Integer from the argument being passed, as clearly the type parameter is a format parameter in the method.

So, yes compiler correctly infers the type as Integer , and hence the ambiguity is valid. And now I think this answer is complete.

Here you go, the difference between the below two methods: Method 1:

   public MyContainer<IntWrapper> pack(int key, Object[] values) {
    return new MyContainer<>(new IntWrapper(""));
   }

Method 2:

public MyContainer<IntWrapper> pack(int key, Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
}

Method 2 is as good as

public MyContainer<IntWrapper> pack(Object ... values) {
    return new MyContainer<>(new IntWrapper(""));
 }

That is why you get an ambiguity..

EDIT Yes I want to say that they are the same for the compile. The whole purpose of using variable arguments is to enable a user to define a method when he/she is not sure about the number of arguments of a given type.

So if you are using an object as variable arguments, you just say the compiler that I am not sure how many objects I will send and on the other hand, you are saying,"I am passing an integer and unknown number of objects". For the compiler the integer is an object as well.

If you want to check the validity try passing an integer as the first argument and then pass a variable argument of String. You will see the difference.

For eg:

public class Converter {
public static void a(int x, String... y) {
}

public static void a(String... y) {
}

public static void main(String[] args) {
    a(1, "2", "3");
}
}

Also, please do not use the arrays and variable args interchangeably, they have some different purposes altogether.

When you use varargs the method doesn't expect an array but different parameters of the same type, which can be accessed in an indexed manner.

In this case

(1) m(K,   String[])
(2) m(int, String[])

m(1, new String[]{..});

m(1) satisfies 15.12.2.3. Phase 2: Identify Matching Arity Methods Applicable by Method Invocation Conversion

m(2) satisfies 15.12.2.2. Phase 1: Identify Matching Arity Methods Applicable by Subtyping

Compiler stops at Phase 1; it found m(2) as the only applicable method at that phase, therefore m(2) is chosen.

In the var arg case

(3) m(K,   String...)
(4) m(int, String...)

m(1, str1, str2);

Both m(3) and m(4) satisfy 15.12.2.4. Phase 3: Identify Applicable Variable Arity Methods . Neither is more specific than the other, therefore the ambiguity.

We can group applicable methods into 4 groups:

  1. applicable by subtyping
  2. applicable by method invocation conversion
  3. vararg, applicable by subtyping
  4. vararg, applicable by method invocation conversion

The spec merged group 3 and 4 and treat them both in Phase 3. Therefore the inconsistency.

Why did they do that? Maye they just got tired of it.

Another critique would be that, there shouldn't be all these phases, because programmers don't think in that way. We should simply find all applicable methods indiscriminately, then choose the most specific one (with some mechanism to avoid boxing/unboxing)

First of all, this is only some first clues... may edit for more.

The compiler always searches and selects the most specific method available. Though a bit clumsy to read, it's all specified in JLS 15.12.2.5 . Thus, by calling

converter.pack(1, "Test", "Test2" )

it's not determinable for the compiler whether the 1 shall be dissolved to K or int . In other words, K can apply to any type, so it's at the same level as int/Integer.

The difference lies in the number and type of the arguments. Consider that new String[]{"Test", "Test2"} is an array, while "Test", "Test2" are two arguments of type String!

converter.pack(1); // ambiguous, compiler error

converter.pack(1, null); // calls method 2, compiler warning

converter.pack(1, new String[]{}); // calls method 2, compiler warning

converter.pack(1, new Object());// ambiguous, compiler error

converter.pack(1, new Object[]{});// calls method 2, no warning

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM