6.3 Method Overloading Principles
Key Takeaways
- Method overloading requires methods within the same class to share the exact same identifier while having distinct formal parameter lists differing by count, data types, or type order.
- Changing only the return type, access modifier, parameter variable names, or declared throws clause does not overload a method and results in a fatal duplicate method compile error.
- The Java compiler resolves overloaded method calls at compile time using a strict four-phase hierarchy: exact match, primitive widening, autoboxing/unboxing, and variable-arity (varargs).
- Primitive widening takes precedence over autoboxing, and the compiler strictly prohibits combining primitive widening with autoboxing in a single method argument conversion.
- Overloaded methods with symmetric parameter widening conversions cause a fatal compile-time ambiguity error when invoked with arguments that match multiple signatures equally.
6.3 Method Overloading Principles
[!NOTE] Exam Focus: Method overloading represents Oracle's "create overloaded methods" objective, and one of the most heavily emphasized ideas on the 1Z0-811 examination. Candidates must be prepared to determine whether a group of method declarations constitutes legal overloading or an illegal duplicate method error, trace compiler resolution across primitive widening and autoboxing hierarchies, recognize varargs resolution rules, and spot compiler ambiguity errors.
Java provides multiple mechanisms for implementing polymorphism—the object-oriented principle that allows a single interface or identifier to represent different underlying behaviors. While method overriding represents runtime dynamic polymorphism based on inheritance, method overloading represents compile-time static polymorphism (also referred to as ad-hoc polymorphism). Method overloading enables a class to expose multiple operations under a unified, intuitive method name tailored to different parameter counts and data types.
What Constitutes Valid Method Overloading
Under JLS §8.4.9, method overloading occurs when two or more methods within the same class (or inherited from a superclass) share the exact same method identifier but possess different formal parameter lists.
Valid Overloading Criteria
To successfully overload a method, the parameter list must differ in at least one of three distinct ways:
- Different Number of Parameters:
public void printReport(String title) { } public void printReport(String title, int copies) { } // LEGAL: Different parameter count - Different Data Types of Parameters:
public void process(int code) { } public void process(String code) { } // LEGAL: Different parameter types - Different Order of Parameter Data Types:
public void logEntry(int code, String message) { } public void logEntry(String message, int code) { } // LEGAL: Different parameter type order
What Does NOT Constitute Method Overloading
A method's signature consists strictly of its identifier and its ordered formal parameter types. Consequently, altering any other component of a method declaration while leaving the name and parameter types identical does not overload the method and results in a fatal compile-time error (method ... is already defined):
| Attempted Variation | Code Pair Example | Compiler Result |
|---|---|---|
| Changing Return Type Only | public int compute(int x)<br/>public double compute(int x) | COMPILE ERROR: duplicate method compute(int) |
| Changing Access Modifier Only | public void send(String s)<br/>private void send(String s) | COMPILE ERROR: duplicate method send(String) |
| Changing Parameter Names Only | void draw(int width, int height)<br/>void draw(int x, int y) | COMPILE ERROR: duplicate method draw(int, int) |
| Changing Exception Specification Only | void read() throws IOException<br/>void read() throws SQLException | COMPILE ERROR: duplicate method read() |
Changing static Modifier Only | public static void run(int n)<br/>public void run(int n) | COMPILE ERROR: duplicate method run(int) |
Rationale: Why Return Types Cannot Overload Methods
Consider what would happen if Java allowed overloading by return type alone:
// HYPOTHETICAL INVALID CODE
public int getScore() { return 100; }
public double getScore() { return 100.0; }
When a caller invokes the method as a freestanding statement without capturing the result:
getScore();
Because the caller does not assign the result to any typed variable, the compiler would have no syntactic or semantic clue whether the caller intended to invoke the int or double variant. To prevent this inherent ambiguity, return types are completely excluded from method signatures and overload resolution.
Overload Resolution Hierarchy
When an overloaded method is invoked, the Java compiler determines which method version to bind at compile time using a deterministic, multi-tier resolution algorithm defined in JLS §15.12.2:
Invocation Call: e.g., execute(10)
│
▼
Phase 1: Exact Parameter Type Match (Identity Conversion)
│ (If no matching method is found)
▼
Phase 2: Primitive Widening Conversion (e.g., int -> long -> float -> double)
│ (If no matching method is found)
▼
Phase 3: Autoboxing / Unboxing Conversion (e.g., int -> Integer)
│ (If no matching method is found)
▼
Phase 4: Variable-Arity (Varargs) Invocation (e.g., int...)
│ (If no matching method is found)
▼
Compilation Error: No suitable method found
Tracing Resolution Precedence
Consider this comprehensive test class:
public class ResolutionOrder {
public static void display(double d) { System.out.println("Widening (double)"); }
public static void display(Integer i) { System.out.println("Autoboxing (Integer)"); }
public static void display(int... v) { System.out.println("Varargs (int...)"); }
public static void main(String[] args) {
int value = 42;
display(value); // Which method is selected by the compiler?
}
}
- Phase 1 (Exact Match): The compiler looks for
display(int). None exists. - Phase 2 (Widening): The compiler checks if
intcan be widened. Primitive widening allowsintto widen tolong,float, ordouble. Sincedisplay(double)is declared, it matches in Phase 2! - The program prints:
Widening (double). - Autoboxing to
Integeris not evaluated because Phase 2 succeeded. Varargs is evaluated only if all previous phases fail.
Critical Resolution Constraints and Edge Cases
1. Primitive Widening Beats Autoboxing
As demonstrated above, Java prioritizes primitive widening over boxing conversion. Widening was built into the language in Java 1.0 to preserve backward compatibility, whereas autoboxing was added in Java SE 5.
2. Prohibited Combined Conversion: Widening Followed by Boxing
While Java allows widening followed by subtyping (e.g., int widening to long and then passing to a parameter expecting long), the compiler strictly forbids primitive widening followed by autoboxing in method invocation conversions:
public class WideningBoxingTrap {
public static void process(Long obj) {
System.out.println("Long object: " + obj);
}
public static void main(String[] args) {
// COMPILE ERROR: incompatible types: int cannot be converted to Long
process(10);
// LEGAL: 10L is a long literal, which boxes directly to Long
process(10L);
}
}
In the code above, 10 is an int. For process(10) to match process(Long), the int would first have to widen to long and then box to Long. Java does not perform this compound conversion.
3. Permitted Combined Conversion: Boxing Followed by Widening (to Object/Number)
Conversely, Java does permit autoboxing followed by reference widening to a superclass or interface:
public static void examine(Object obj) { System.out.println("Object accepted"); }
// Calling examine with primitive int:
examine(100); // LEGAL! int boxes to Integer, and Integer widens (subtypes) to Object!
4. Varargs as the Choice of Last Resort
Methods declaring variable-arity parameters (...) are relegated to Phase 4. If any fixed-arity method matches via exact match, widening, or boxing, the compiler selects the fixed-arity method:
public static void test(int a, int b) { System.out.println("Fixed-arity"); }
public static void test(int... nums) { System.out.println("Varargs"); }
test(1, 2); // Prints: Fixed-arity!
test(1, 2, 3); // Prints: Varargs!
Compiler Ambiguity Errors
When the compiler finds two or more overloaded methods that match an invocation equally well without one being strictly more specific than the other, it cannot guess the programmer's intent and raises a compile-time error: reference to ... is ambiguous.
1. Symmetric Primitive Widening Ambiguity
public class AmbiguityDemo {
public static void add(int a, double b) {
System.out.println("int, double");
}
public static void add(double a, int b) {
System.out.println("double, int");
}
public static void main(String[] args) {
// COMPILE ERROR: reference to add is ambiguous
add(5, 5);
}
}
Why Compilation Fails: The argument pair (5, 5) consists of two int literals. For the first method add(int, double), the second argument must widen (int -> double). For the second method add(double, int), the first argument must widen (int -> double). Because each method requires exactly one widening conversion, neither method is more specific. The compiler rejects the code.
To resolve this ambiguity, the caller must explicitly cast one argument:
add(5, (double) 5); // Clearly invokes add(int, double)
2. Null Reference Ambiguity Between Overloaded Object Types
Another classic exam scenario involves passing null to overloaded methods taking unrelated object types:
public class NullAmbiguity {
public static void inspect(String s) { System.out.println("String"); }
public static void inspect(Integer i) { System.out.println("Integer"); }
public static void main(String[] args) {
// COMPILE ERROR: reference to inspect is ambiguous
inspect(null);
}
}
Because null is a valid literal for both String and Integer, and neither type inherits from the other, the compiler cannot determine which method takes priority and generates a compile-time error. If one type were a subtype of the other (e.g., String and Object), the compiler would choose the most specific type (String).
Which of the following method declarations represents a legally valid overload of the method:
public int calculate(int x, double y)
Consider the following class with three overloaded methods:
What is printed to the console upon running this program?public class OverloadPrecedence {
public static void check(long val) { System.out.println("long"); }
public static void check(Integer val) { System.out.println("Integer"); }
public static void check(int... val) { System.out.println("varargs"); }
public static void main(String[] args) {
int num = 10;
check(num);
}
}
What is the result of attempting to compile and execute the following Java class?
public class AmbiguityTest {
public static void combine(int a, double b) {
System.out.println("int-double");
}
public static void combine(double a, int b) {
System.out.println("double-int");
}
public static void main(String[] args) {
combine(10, 20);
}
}