javaoverloadingoverload-resolution

How does Java resolve method overloading ambiguity when competing methods have parameters with similar depth hierarchy


Here's a very basic program I wrote to test how Java overloading is resolved when an exact match is not found, and what priority is assigned to other matching methods.

import java.io.*;
public class Main
{
   
   
    //NOTE: Comparable<Integer> ties with Serializable and with Number, however Number wins over Serializable
   
    // static void test(Number x) {
    //     System.out.println("here in Number x");
    // }
   
   
    // static void test(Serializable x) {
    //     System.out.println("here in Serializable x");
    // }
   
    // static void test(Comparable<Integer> x) {
    //     System.out.println("here in Comparable<Integer> x");
    // }

   
public static void main(String[] args) {
    test(10);
    }
}

When test(Number x) is commented out: test(Comparable\<Integer\> x) gives an ambiguous reference error when it's present alongside test(Serializable x).

When test(Serializable x) is commented out The same happens for test(Comparable\<Integer\> x) and test(Number x).

So I assumed that all three of these must be interpreted as having the same hierarchy depth by the JVM. This makes sense, since Integer subclasses Number and implements Serializable and Comparable<Integer>.

However, when I comment out the Comparable<Integer> method, then Number wins over Serializable, and test(Number x) is called

To put it simply, I assume a sort of transitivity where if method A clashes with method B, and method A also clashes with method C, then method B must also clash with method C, but that's not the case.

Can someone please explain what's going on here?

For context, I used an online compiler to test the code. I mention this because I'm not sure if this behavior is JVM implementation-specific.

Edit: I did come across method overloading priority in Java, but that does not answer my question. That material explains why certain methods are prioritized over others, but I am asking: why is it that when method A ties with method B and C, method B and method C don't give the same ambiguity error? Maybe I'm missing something very basic here.

Edit 2: I removed the additional question about varargs (as suggested by the reviewer). Maybe I'll post another question for that.


Solution

  • How does Java resolve method overloading ambiguity with parent classes and interfaces are involved?

    The Java Language Specification is the authoritative reference for overload resolution and all other details of Java language semantics. In JLS 25, resolving the specific method chosen for a method invocation is one of the main topics covered in section 15.12. Overload resolution is part of determining the method signature (JLS25 15.12.2). The summary description is:

    The second step searches the type determined in the previous step for member methods. This step uses the name of the method and the argument expressions to locate methods that are both accessible and applicable, that is, declarations that can be correctly invoked on the given arguments.

    There may be more than one such method, in which case the most specific one is chosen. The descriptor (signature plus return type) of the most specific method is the one used at run time to perform the method dispatch.

    (Italics in the original.) The second paragraph is about overloaded methods, and the last sentence is the theme that the ensuing rules for overload resolution are meant to implement.

    1. The first phase 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.

    That's approximately widening -> (boxing, varargs) in the notation presented in the question. (And note that upcasting is a form of widening conversion). Among your example methods, including the commented-out ones, that rule would distinguish test(long) above all the others, but it would not distinguish among the rest.

    1. The second phase 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.

    That's approximately (boxing, boxing + widening) -> (varargs (+ anything)). It distinguishes the remaining non-variadic methods from the variadic ones, but it does not distinguish among the several members of either of those groups.

    And for completeness:

    1. The third phase allows overloading to be combined with variable arity methods, boxing, and unboxing.

    When application of those rules produces multiple applicable candidates (I do not cover details of applicability in this answer), an attempt is made to choose a most specific one of them. This is detailed in JLS25 15.12.2.5. The summary description is:

    The informal intuition is that one method is more specific than another if any invocation handled by the first method could be passed on to the other one without a compile-time error. In cases such as an explicitly typed lambda expression argument (§15.27.1) or a variable arity invocation (§15.12.2.4), some flexibility is allowed to adapt one signature to the other.

    Note well that "more specific" is a relation over method signatures alone. The actual arguments to the method invocation have all the effect they ever will on this analysis at the previous stages of identifying applicable methods and selecting among those based on whether (un)boxing conversions or variable-arity invocation would be required.

    It gets pretty technical from there, but here's how it works out among your methods with argument type Integer, Number, Comparable<Integer>, Serializable, Object:

    Among the variadic alternatives, the analysis is based on the declared parameter types of their parameters, in a variable-arity sense (so int, Integer, and Object in your case, as opposed to arrays of those). It looks at whether all the parameter types of one signature are the same as or more specific (as that applies to types) than the corresponding parameter types of the other. The details are again technical, but for your test(int...), test(Integer...), and test(Object...), it works out that