Today, I'm going to shed some light on the mechanics of method resolution for overloaded and overridden methods in Java.
Will start with a small piece of code:
package net.progsign.prep;
class Base {
public void method(int... params) { //Method #1
System.out.println("[B] int...");
}
public void method(int param1, long param2) { //Method #2
System.out.println("[B] int, long");
}
public void method(int param1, Integer param2) { //Method #3
System.out.println("[B] int, Integer");
}
public void method(int param1, Number param2) { //Method #4
System.out.println("[B] int, Number");
}
public void method(int param1, Object param2) { //Method #5
System.out.println("[B] int, Object");
}
public void method(int param1, int... param2) { //Method #6
System.out.println("[B] int, int...");
}
public void method(int param1, long... param2) { //Method #7
System.out.println("[B] int, long...");
}
public void method(int param1, Integer... param2) { //Method #8
System.out.println("[B] int, Integer...");
}
}
public class OverloadingTest {
public static final void main(String[] args) {
Base base0 = new Base();
base0.method(1, 1);
}
}
The code, when compiled and executed, will result in the following message printed to the console:
[B] int, long //Method #2
OK, so what exactly happend, and how did the virtual machine figure out which method should be executed?
With reference to Java Language Specification for Java SE 7 Edition, there are three phases of resolving the proper method signature:
Phase #1: performs overload resolution without permitting boxing or unboxing conversion, or the use of variable arity method invocation.
During this phase the compiler will try to find an exact match (overloaded method which parameters types match the invocation). If no such method is found, the compiler then will try to upcast (widen) the argument types until the closest match is found.
Below diagram shows the direction the upcasting will process for primitive types:
For non-primitive types (instances) it will follow up the inheritance tree. If no applicable method is found during this phase then processing continues to the next phase.
Phase #2: performs overload resolution while allowing boxing and unboxing, but still precludes the use of variable arity method invocation.
In this phase the compiler will try to utilize the auto-boxing mechanism introduced in Java 5. If boxing (wrapping primitive type with its istance representation) occurs, then, same as before the whole class hierarchy is taken into account, however the most specific type takes precedence over the more generic types.
Example:
package net.progsign.prep;
class Base {
public void method(int param1, Integer param2) { //Method #3
System.out.println("[B] int, Integer");
}
public void method(int param1, Number param2) { //Method #4
System.out.println("[B] int, Number");
}
public void method(int param1, Object param2) { //Method #5
System.out.println("[B] int, Object");
}
}
public class OverloadingTest {
public static final void main(String[] args) {
Base base0 = new Base();
base0.method(1, 1); //Line #1
base0.method(1, null); //Line #2
}
}
The result will be as follows:
[B] int, Integer //Method #3
[B] int, Integer //Method #3
In line marked as Line #1 the second argument was automatically boxed to an Integer. In Line #2 the second argument is null, but since Integer is more specific type than Number and Object, the compiler will favour Method #3 over the other two. If the compiler is still not satisfied, it continues to the third phase.
Phase #3: allows overloading to be combined with variable arity methods, boxing, and unboxing.
As you can see, varargs (short for "variable arguments", also introduced in Java 5) have the lowest priority. This is to ensure backward compatibility with legacy code that doesn't support this feature.
Knowing the above rules, there should be no problem with predicting the output of the following code:
package net.progsign.prep;
class Base {
public void method(int... params) { //Method #1
System.out.println("[B] int...");
}
public void method(int param1, int param2) { //Method #2
System.out.println("[B] int, int");
}
public void method(int param1, Integer param2) { //Method #3
System.out.println("[B] int, Integer");
}
}
public class OverloadingTest {
public static final void main(String[] args) {
Base base0 = new Base();
base0.method(1, 1);
}
}
Of course Method #2 will be invoked first, and in case of its absence the compiler will prefer Method #3 rather than Method #1. Simple enough.
OK, what about this code:
package net.progsign.prep;
class Base {
public void method(short... params) { //Method #1
System.out.println("[B] short...");
}
public void method(int param1, int param2) { //Method #2
System.out.println("[B] int, int");
}
public void method(short param1, double param2) { //Method #3
System.out.println("[B] short, double");
}
}
public class OverloadingTest {
public static final void main(String[] args) {
Base base0 = new Base();
base0.method((short)2, (short)5);
}
}
This is where things get a bit tricky. From earlier we know that Method #1 will be the last to take into consideration. But what about the other two methods?
There's section 15.12.2.5. Choosing the Most Specific Method in the JLS that explains the rules of selecting the most specific method signature.
Here's what is going to happen in the above code. The compiler will compare the first parameter and will find a perfect match in Method #3. Spot on! But when it gets to the second parameter it will actually lean forward Method #2 (see the upcasting chart). This ambiguity will lead to a compiler error.
Having more fun!
Hope everything makes a bit more sense now. Let's add some petrol to the fire:
package net.progsign.prep;
class Base {
public void method(int... params) { //Method #1
System.out.println("[B] int...");
}
public void method(long param1, double param2) { //Method #2
System.out.println("[B] long, double");
}
}
class Subclass extends Base {
public void method(int... params) {
System.out.println("[D] int..."); //Method #S1
}
public void method(int param1, int param2) { //Method #S2
System.out.println("[D] int, int");
}
public void method(int param1, int... param2) { //Method #S3
System.out.println("[D] int, int...");
}
}
public class OverloadingTest {
public static final void main(String[] args) {
Base base0 = new Base();
Base base1 = new Subclass();
Subclass subclass0 = new Subclass();
Subclass subclass1 = (Subclass)base1;
base0.method(1, 1); //Line #1
base1.method(1, 1); //Line #2
subclass0.method(1, 1); //Line #3
subclass1.method(1, 1); //Line #4
}
}
And the output will be...
[B] long, double
[B] long, double
[D] int, int
[D] int, int
Right as expected, isn't it? So what have we got here. Class Base declaring two overloaded methods. Also another class, Subclass, with three more overloaded (overriden?) methods.
In the main function we first instantiate Base class, and refer to it by its base type. Then we create an instance of a Subclass class, but we're using its super type as a reference. Also another class is being created and assigned to a proper reference type. And finally another reference of type Subclass is created to point to one of the previously created objects.
I believe Line #1 is clear enough. But what actually happened in Line #2? In Line #1 the compiler resolved Method #2 to be the best match of the Base class. Since we're using the same reference type in Line #2, the same Method #2 was invoked again. Please note that the only overriden method in the Subclass is Method #1 (overriden by Method #S1, so there's no polymorphism taking place in this case. Also, because Base doesn't know anything about Method #S2, which otherwise would be the best candidate, that method is not called. There's no black magic in Line #3 and Line #4.
One last thing to remember: In a subclass, you can overload the methods inherited from the superclass. Such overloaded methods neither hide nor override the superclass instance methods—they are new methods, unique to the subclass.