Java overloading rules

2019-01-07 19:08发布

问题:

I came across two overloading questions recently that I can't find an answer for and don't have a java environment to run some test code. I'm hoping someone can help me by assembling a list of all the rules that java compilers follow follow for overloading or alternately pointing me at a list that already exists.

First, when two methods differ only by a final varargs parameter, under what circumstances do each get called and can you call the varargs method without any args?

private void f(int a) { /* ... */ }
private void f(int a, int... b) { /* ... */ }

f(12); // calls the former? I would expect it to
f(12, (int[])null); // calls latter, but passes null for b? 
  // Can I force the compiler to call the second method in the same fashion
  // as would happen if the first method didn't exist?

Second question, when two methods differ by types inherited from one another which gets called? I'd expect the most derived version to be called, and casting allowed to call the other.

interface A {}
class B implements A {}
class C implements A {}

private void f(A a) {}
private void f(B b) {}

f(new C()); // calls the first method
f(new B()); // calls the second method?
f((A)(new B()); // calls the first method using a B object?

These are the two examples, but as a code-reader I'd prefer a canonical list of the exact ordered rules used for resolving this as I frequently don't have time to setup a build environment to check what the compiler is doing.

回答1:

Overloading vs Overriding

The selection of the right implementation of method is done at runtime as you well pointed out, now the signature of the method to be invoked is decided at compile time.

Overloading Method Selection at Compile Time

The Java Language Specification (JLS) in section 15.12 Method Invocation Expressions explains in detail the process that the compiler follows to choose the right method to invoke.

There, you will notice that this is a compile-time task. The JLS says in subsection 15.12.2:

This step uses the name of the method and the types of the argument expressions to locate methods that are both accessible and applicable There may be more than one such method, in which case the most specific one is chosen.

Typically, varargs methods are the last chosen, if they compete with other candidate methods, because they are considered less specific than the ones receiving the same parameter type.

To verify the compile-time nature of this, you can do the following test.

Declare a class like this and compile it.

public class ChooseMethod {
   public void doSomething(Number n){
    System.out.println("Number");
   }
}

Declare a second class that invokes a method of the first one and compile it.

public class MethodChooser {
   public static void main(String[] args) {
    ChooseMethod m = new ChooseMethod();
    m.doSomething(10);
   }
}

If you invoke the main, the output says Number.

Now, add a second more specific overloaded method to the ChooseMethod class, and recompile it (but do not recompile the other class).

public void doSomething(Integer i) {
 System.out.println("Integer");
}

If you run the main again, the output is still Number.

Basically, because it was decided at compile time. If you recompile the MethodChooser class (the one with the main), and run the program again, the output will be Integer.

As such, if you want to force the selection of one of the overloaded methods, the type of the arguments must correspond with the type of the parameters at compile time, and not only at run time.

Overriding Method Selection at Run time

Again, the signature of the method is decided at compile time, but the actual implementation is decided at runtime.

Declare a class like this and compile it.

public class ChooseMethodA {
   public void doSomething(Number n){
    System.out.println("Number A");
   }
}

Then declare a second extending class and compile:

public class ChooseMethodB extends ChooseMethodA {  }

And in the MethodChooser class you do:

public class MethodChooser {
    public static void main(String[] args) {
        ChooseMethodA m = new ChooseMethodB();
        m.doSomething(10);
    }
}

And if you run it you get the output Number A, and this is Ok, because the method has not been overriden in ChooseMethodB and therefore the implementation being invoked is that of ChooseMethodA.

Now, add an overriden method in MethodChooserB:

public void doSomething(Number n){
    System.out.println("Number B");
}

And recompile just this one, and run the main method again.

Now, you get the output Number B

As such, the implementation was chosen at runtime, and not recompilation of the MethodChooser class was required.



回答2:

First question:

Your assumption is correct. The second call to f() will call the varargs method. You can get the compiler to call the second method with:

private void f(int a) {
    f(a, null);
}

Second question:

Yes. However, you can't extend an interface. If you change A to an abstract class, things will compile.