Overloaded Method Resolution
In this subsection, we take a look at some aspects regarding overloaded method resolution—that is, how the compiler determines which overloaded method will be invoked by a given method call at runtime.
Resolution of overloaded methods selects the most specific method for execution. One method is considered more specific than another method if all actual parameters that can be accepted by the one method can be accepted by the other method. If more than one such method is present, the call is described as ambiguous. The following overloaded methods illustrate this situation:
private static void flipFlop(String str, int i, Integer iRef) { // (1)
out.println(str + ” ==> (String, int, Integer)”);
}
private static void flipFlop(String str, int i, int j) { // (2)
out.println(str + ” ==> (String, int, int)”);
}
Their method signatures are as follows:
flipFlop(String, int, Integer) // See (1)
flipFlop(String, int, int) // See (2)
The following method call is ambiguous:
flipFlop(“(String, Integer, int)”,
Integer.valueOf(4), 2020); // (3) Ambiguous call
It has the call signature:
flipFlop(String, Integer, int) // See (3)
The method at (1) can be called with the second argument unboxed and the third argument boxed, as can the method at (2) with only the second argument unboxed. In other words, for the call at (3), none of the methods is more specific than the other.
Example 5.19 illustrates a simple case of how method resolution is done to choose the most specific overloaded method. The method testIfOn() is overloaded at (1) and (2) in the class Overload. The call client.testIfOn(tubeLight) at (3) satisfies the parameter lists in both implementations given at (1) and (2), as the reference tube-Light can also be assigned to a reference of its superclass Light. The most specific method, (2), is chosen, resulting in false being written on the terminal. The call client.testIfOn(light) at (4) satisfies only the parameter list in the implementation given at (1), resulting in true being written on the terminal. This is also the case at (5). The object referred to by the argument in the call at runtime is irrelevant; rather, it is the type of the argument that is important for overloaded method resolution.
Example 5.19 Choosing the Most Specific Method (Simple Case)
class Light { /* … */ }
class TubeLight extends Light { /* … */ }
public class Overload {
boolean testIfOn(Light aLight) { return true; } // (1)
boolean testIfOn(TubeLight aTubeLight) { return false; } // (2)
public static void main(String[] args) {
TubeLight tubeLight = new TubeLight();
Light light = new Light();
Light light2 = new TubeLight();
Overload client = new Overload();
System.out.println(client.testIfOn(tubeLight)); // (3) ==> method at (2)
System.out.println(client.testIfOn(light)); // (4) ==> method at (1)
System.out.println(client.testIfOn(light2)); // (5) ==> method at (2)
}
}
Output from the program:
false
true
true
The algorithm used by the compiler for the resolution of overloaded methods incorporates the following phases:
The compiler performs overload resolution without permitting boxing, unboxing, or the use of a variable arity call.
If phase (1) fails, the compiler performs overload resolution allowing boxing and unboxing, but excluding the use of a variable arity call.
If phase (2) fails, the compiler performs overload resolution combining a variable arity call, boxing, and unboxing.
Example 5.20 provides some insight into how the compiler determines the most specific overloaded method using these three phases. The example has six overloaded declarations of the method action(). The signature of each method is given by the local variable signature in each method. The first formal parameter of each method is the signature of the call that invoked the method. The printout from each method allows us to see which method call resolved to which method. The main() method contains 10 calls, (8) to (17), of the action() method. In each call, the first argument is the signature of that method call.
An important point to note is that the compiler chooses a fixed arity call over a variable arity call, as seen in the calls from (8) to (12):
(String) => (String) (8) calls (1)
(String, int) => (String, int) (9) calls (2)
(String, Integer) => (String, int) (10) calls (2)
(String, int, byte) => (String, int, int) (11) calls (3)
(String, int, int) => (String, int, int) (12) calls (3)
An unboxing conversion (Integer to int) takes place for the call at (10). A widening primitive conversion (byte to int) takes place for the call at (11).
Variable arity calls are chosen from (13) to (17):
(String, int, long) => (String, Number[]) (13) calls (5)
(String, int, int, int) => (String, Integer[]) (14) calls (4)
(String, int, double) => (String, Number[]) (15) calls (5)
(String, int, String) => (String, Object[]) (16) calls (6)
(String, boolean) => (String, Object[]) (17) calls (6)
When a variable arity call is chosen, the method determined has the most specific variable arity parameter that is applicable for the actual argument. For example, in the method call at (14), the type Integer[] is more specific than either Number[] or Object[]. Note also the boxing of the elements of the implicitly created array in the calls from (13) to (17).
Example 5.20 Overloaded Method Resolution
import static java.lang.System.out;
class OverloadResolution {
public void action(String str) { // (1)
String signature = “(String)”;
out.println(str + ” => ” + signature);
}
public void action(String str, int m) { // (2)
String signature = “(String, int)”;
out.println(str + ” => ” + signature);
}
public void action(String str, int m, int n) { // (3)
String signature = “(String, int, int)”;
out.println(str + ” => ” + signature);
}
public void action(String str, Integer… data) { // (4)
String signature = “(String, Integer[])”;
out.println(str + ” => ” + signature);
}
public void action(String str, Number… data) { // (5)
String signature = “(String, Number[])”;
out.println(str + ” => ” + signature);
}
public void action(String str, Object… data) { // (6)
String signature = “(String, Object[])”;
out.println(str + ” => ” + signature);
}
public static void main(String[] args) {
OverloadResolution ref = new OverloadResolution();
ref.action(“(String)”); // (8) calls (1)
ref.action(“(String, int)”, 10); // (9) calls (2)
ref.action(“(String, Integer)”, Integer.valueOf(10)); // (10) calls (2)
ref.action(“(String, int, byte)”, 10, (byte)20); // (11) calls (3)
ref.action(“(String, int, int)”, 10, 20); // (12) calls (3)
ref.action(“(String, int, long)”, 10, 20L); // (13) calls (5)
ref.action(“(String, int, int, int)”, 10, 20, 30); // (14) calls (4)
ref.action(“(String, int, double)”, 10, 20.0); // (15) calls (5)
ref.action(“(String, int, String)”, 10, “what?”); // (16) calls (6)
ref.action(“(String, boolean)”, false); // (17) calls (6)
}
}
For output from the program, see the explanation for Example 5.20 in the text.
Leave a Reply