Loading learning content...
Method overloading seems straightforward at first: same name, different parameters. But when multiple overloads could potentially match a given call, which one wins? When does the compiler reject ambiguous code? How do overloaded methods interact with inheritance?
These questions matter because overload resolution failures happen at compile time, often with cryptic error messages. Understanding the precise rules transforms debugging frustration into confident API design.
Consider this seemingly simple scenario:
void process(int x, long y) { }
void process(long x, int y) { }
process(5, 5); // Which overload is called?
Neither method is a better match for two int arguments—both require one promotion. This ambiguity causes a compile-time error. Understanding why requires mastering overload resolution rules.
By the end of this page, you will understand the complete overload resolution algorithm: how the compiler identifies applicable methods, ranks them by specificity, and either selects a winner or reports ambiguity. You'll learn the inheritance rules for overloading, static method considerations, and how to design overloads that avoid resolution pitfalls.
When the compiler encounters a method call, it follows a precise algorithm to determine which overloaded method to invoke. This process is called overload resolution.
Phase 1: Gather Candidate Methods
The compiler identifies all methods that:
Phase 2: Determine Applicable Methods
For each candidate, the compiler checks if the provided arguments can be converted to the parameter types through:
Phase 3: Select the Most Specific Method
Among applicable methods, the compiler selects the one that is most specific. Method A is more specific than method B if you could pass A's parameter types to B (but not vice versa).
Phase 4: Report Ambiguity or Proceed
If no single most specific method exists, the compiler reports an ambiguity error.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
public class OverloadResolution { // Candidate methods void calculate(Object obj) { System.out.println("Object"); } void calculate(Number num) { System.out.println("Number"); } void calculate(Integer integer) { System.out.println("Integer"); } void calculate(int primitive) { System.out.println("int"); } public static void main(String[] args) { OverloadResolution resolver = new OverloadResolution(); // Call with int literal 42 resolver.calculate(42); // Phase 1: All 4 methods are candidates // Phase 2: All 4 are applicable // - calculate(int): exact match // - calculate(Integer): requires boxing // - calculate(Number): requires boxing + widening // - calculate(Object): requires boxing + widening // Phase 3: calculate(int) is most specific // Result: prints "int" // Call with Integer Integer wrapped = 42; resolver.calculate(wrapped); // Phase 2: calculate(int) requires unboxing // calculate(Integer): exact match // calculate(Number): widening reference // calculate(Object): widening reference // Phase 3: calculate(Integer) is most specific // Result: prints "Integer" // Call with Number Number number = 42; resolver.calculate(number); // Phase 2: calculate(int): not applicable (can't unbox Number) // calculate(Integer): not applicable (narrowing) // calculate(Number): exact match // calculate(Object): widening // Phase 3: calculate(Number) is most specific // Result: prints "Number" // Call with String resolver.calculate("hello"); // Phase 2: Only calculate(Object) is applicable // Result: prints "Object" }}Think of specificity as: 'Which method describes the arguments most precisely?' A method accepting Integer is more specific than one accepting Number, which is more specific than one accepting Object. The compiler always chooses the most precise description of your arguments.
Not all type conversions are treated equally in overload resolution. The compiler applies conversions in a strict priority order, and this ordering is crucial for understanding which overload gets selected.
The Priority Hierarchy (Java):
| Priority | Conversion Type | Example | Description |
|---|---|---|---|
| 1 (Highest) | Exact Match | int → int | No conversion needed |
| 2 | Widening Primitive | int → long | Lossless primitive promotion |
| 3 | Widening Reference | String → Object | Subtype to supertype |
| 4 | Boxing | int → Integer | Primitive to wrapper |
| 5 | Unboxing | Integer → int | Wrapper to primitive |
| 6 | Boxing + Widening | int → Object | Box then widen reference |
| 7 (Lowest) | Varargs | (a, b, c) → (int...) | Pack into array |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
public class ConversionPriorityDemo { // Priority 1: Exact match void accept(int x) { System.out.println("exact int: " + x); } // Priority 2: Widening primitive void accept(long x) { System.out.println("widened to long: " + x); } // Priority 4: Boxing void accept(Integer x) { System.out.println("boxed Integer: " + x); } // Priority 6: Boxing + widening reference void accept(Object x) { System.out.println("Object: " + x); } // Priority 7: Varargs void accept(int... x) { System.out.println("varargs: " + x.length + " elements"); } public static void main(String[] args) { ConversionPriorityDemo demo = new ConversionPriorityDemo(); demo.accept(42); // Matches accept(int) - exact match wins // Now remove accept(int) and recompile: // demo.accept(42); // Would match accept(long) - widening wins over boxing // Remove accept(long) too: // demo.accept(42); // Would match accept(Integer) - boxing wins // Remove accept(Integer) too: // demo.accept(42); // Would match accept(Object) - box + widen // Remove accept(Object) too: // demo.accept(42); // Would match accept(int...) - varargs as last resort }} class VarargsLast { void test(int x, int y) { System.out.println("Two ints"); } void test(int... x) { System.out.println("Varargs"); } public static void main(String[] args) { VarargsLast demo = new VarargsLast(); demo.test(1, 2); // Prints "Two ints" - specific beats varargs demo.test(1, 2, 3); // Prints "Varargs" - only varargs matches demo.test(1); // Prints "Varargs" - only varargs matches }}Boxing was added in Java 5, and varargs in the same version. The priority order ensures backward compatibility: code written before these features still resolves to the same methods. Widening (which existed since Java 1.0) takes priority over boxing to preserve legacy behavior.
Ambiguity occurs when the compiler cannot determine a single most specific method. This is a compile-time error, not a runtime uncertainty. Understanding common ambiguity patterns helps you design overloads that avoid these pitfalls.
Classic Ambiguity Patterns:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
public class AmbiguityExamples { // ============================================= // Pattern 1: Crossed Parameter Types // ============================================= void handle(int x, long y) { } void handle(long x, int y) { } void testCrossed() { // handle(5, 5); // COMPILE ERROR! // Both require onepromotion: neither is more specific // Solutions: handle(5, 5L); // Explicit: forces handle(int, long) handle((long)5, 5); // Explicit: forces handle(long, int) } // ============================================= // Pattern 2: Primitive vs Wrapper Confusion // ============================================= void process(int x, Integer y) { } void process(Integer x, int y) { } void testPrimitiveWrapper() { int a = 1; int b = 2; // process(a, b); // COMPILE ERROR! // Both require one boxing: neither is more specific // Solutions: process(a, Integer.valueOf(b)); // Forces first overload process(Integer.valueOf(a), b); // Forces second overload } // ============================================= // Pattern 3: Varargs Collisions // ============================================= void log(Object... args) { } void log(String... args) { } void testVarargs() { // log("a", "b"); // COMPILE ERROR in some cases! // String[] is more specific than Object[], but varargs // resolution can be tricky with null or mixed types log(new String[]{"a", "b"}); // Explicit array avoids ambiguity } // ============================================= // Pattern 4: Generics Erasure Ambiguity // ============================================= // void process(List<String> list) { } // void process(List<Integer> list) { } // COMPILE ERROR: Both erase to process(List) at runtime! // Solution: Use different method names or add discriminating parameters void processStrings(List<String> list) { } void processIntegers(List<Integer> list) { }}Due to type erasure, List<String> and List<Integer> are the same type at runtime: List. You cannot overload methods based solely on generic type arguments. This is a fundamental limitation of Java's generics implementation. C# and other languages with reified generics don't have this restriction.
Resolution: When Neither Method is More Specific
The compiler reports ambiguity when it finds multiple applicable methods and cannot determine a winner. The key insight is that specificity comparison must be total—one method must be clearly more specific than all others.
Partial Ordering Problem:
1234567891011121314151617181920212223
class PartialOrderingDemo { // Three methods with complex relationships void method(Object a, String b) { } void method(String a, Object b) { } void method(Object a, Object b) { } void test() { String s = "hello"; method(s, s); // COMPILE ERROR! // method(Object, String) requires widening for first arg // method(String, Object) requires widening for second arg // Neither is more specific than the other // method(Object, Object) is applicable but LESS specific than both // Having a less-specific option doesn't resolve the tie between // the more-specific ambiguous options // Solution: Cast to force a choice method((Object)s, s); // Forces method(Object, String) method(s, (Object)s); // Forces method(String, Object) }}When inheritance enters the picture, method overloading gains additional complexity. Subclasses can add new overloads that interact with inherited methods, sometimes in surprising ways.
Key Principle: Overloading Across Class Hierarchy
Overloaded methods don't have to be declared in the same class. A subclass can add new overloads that work alongside inherited methods.
123456789101112131415161718192021222324252627282930313233343536
class Parent { void process(Object obj) { System.out.println("Parent.process(Object): " + obj); }} class Child extends Parent { // This OVERLOADS (not overrides) Parent.process // Different signature: String vs Object void process(String str) { System.out.println("Child.process(String): " + str); }} public class InheritanceOverloadDemo { public static void main(String[] args) { Child child = new Child(); child.process("hello"); // Child.process(String) - exact match child.process(42); // Parent.process(Object) - inherited child.process((Object)"hello"); // Parent.process(Object) - explicit cast // Critical: Overload resolution uses compile-time types Parent parentRef = child; parentRef.process("hello"); // Parent.process(Object)! // The compile-time type is Parent, which only has process(Object) // Child.process(String) is not visible through Parent reference }} /* Output:Child.process(String): helloParent.process(Object): 42Parent.process(Object): helloParent.process(Object): hello <-- Surprising to many developers!*/When you hold a subclass object through a parent class reference, you can only see overloads defined in (or above) the parent class. Overloads added by the subclass are invisible at compile time. This often surprises developers expecting runtime method selection.
Overloading vs Overriding: The Critical Distinction
It's essential to distinguish overloading (compile-time, different signatures) from overriding (runtime, same signature). A common mistake is accidentally overloading when you meant to override:
12345678910111213141516171819202122232425262728293031323334353637383940414243
class Animal { public void speak(String message) { System.out.println("Animal says: " + message); }} class Dog extends Animal { // MISTAKE: This OVERLOADS, not overrides! // Different signature: void speak() vs void speak(String) public void speak() { System.out.println("Woof!"); } // This correctly OVERRIDES @Override public void speak(String message) { System.out.println("Dog barks: " + message); }} public class OverloadOverrideConfusion { public static void main(String[] args) { Animal animal = new Dog(); animal.speak("Hello"); // Uses Dog's override: "Dog barks: Hello" // animal.speak(); // COMPILE ERROR! Animal doesn't have speak() Dog dog = new Dog(); dog.speak("Hello"); // "Dog barks: Hello" dog.speak(); // "Woof!" - Dog's additional overload }} // The @Override annotation is your friend:class Cat extends Animal { // @Override // public void speak() { } // Compiler would flag: not overriding anything! @Override // Compiler verifies this actually overrides public void speak(String message) { System.out.println("Meow: " + message); }}When you intend to override a parent method, always use the @Override annotation. The compiler will verify that you're actually overriding something. If you accidentally create a different signature (thus overloading), the compiler will catch it immediately. This simple practice prevents countless bugs.
Static methods can be overloaded just like instance methods. The rules are identical: different parameter signatures create different static methods sharing the same name. However, static methods have unique characteristics when combined with inheritance.
Static Methods Can Be Overloaded:
123456789101112131415161718192021222324252627282930313233343536
public class MathUtils { // Multiple overloaded static methods public static int max(int a, int b) { return a > b ? a : b; } public static int max(int a, int b, int c) { return max(max(a, b), c); } public static double max(double a, double b) { return a > b ? a : b; } public static <T extends Comparable<T>> T max(T a, T b) { return a.compareTo(b) > 0 ? a : b; } public static int max(int[] values) { if (values.length == 0) throw new IllegalArgumentException(); int result = values[0]; for (int i = 1; i < values.length; i++) { result = max(result, values[i]); } return result; } public static void main(String[] args) { System.out.println(max(5, 3)); // max(int, int) System.out.println(max(5, 3, 7)); // max(int, int, int) System.out.println(max(5.0, 3.0)); // max(double, double) System.out.println(max("apple", "zebra")); // max(T, T) System.out.println(max(new int[]{1,5,3,2})); // max(int[]) }}Static Methods and Inheritance: Hiding, Not Overriding
A critical distinction: static methods in a subclass with the same signature as a parent's static method hide (not override) the parent method. This has significant implications for polymorphic behavior:
12345678910111213141516171819202122232425262728293031323334353637383940414243
class Parent { public static void staticMethod() { System.out.println("Parent static"); } public void instanceMethod() { System.out.println("Parent instance"); }} class Child extends Parent { // This HIDES Parent.staticMethod (not override) public static void staticMethod() { System.out.println("Child static"); } // This OVERRIDES Parent.instanceMethod @Override public void instanceMethod() { System.out.println("Child instance"); }} public class StaticHidingDemo { public static void main(String[] args) { Parent parent = new Parent(); Child child = new Child(); Parent childAsParent = new Child(); // Instance methods: polymorphic behavior parent.instanceMethod(); // "Parent instance" child.instanceMethod(); // "Child instance" childAsParent.instanceMethod(); // "Child instance" - runtime dispatch! // Static methods: NO polymorphic behavior Parent.staticMethod(); // "Parent static" Child.staticMethod(); // "Child static" // The following use compile-time type, not runtime type: parent.staticMethod(); // "Parent static" (don't do this!) child.staticMethod(); // "Child static" (don't do this!) childAsParent.staticMethod(); // "Parent static" - NOT Child's method! }}Calling static methods on object instances (child.staticMethod()) is legal but misleading—it uses the compile-time type, ignoring the actual object. Always call static methods on the class name (ClassName.staticMethod()) to make the behavior explicit and avoid confusion.
Several edge cases in method overloading can catch even experienced developers off guard. Understanding these helps you write more robust code and avoid subtle bugs.
Edge Case 1: Null Arguments
Null is assignable to any reference type, which creates ambiguity when multiple overloads accept reference types:
123456789101112131415161718192021222324252627282930
class NullAmbiguity { void process(String s) { System.out.println("String: " + s); } void process(Integer i) { System.out.println("Integer: " + i); } void process(Object o) { System.out.println("Object: " + o); } public static void main(String[] args) { NullAmbiguity demo = new NullAmbiguity(); // demo.process(null); // COMPILE ERROR! // null matches both String and Integer equally // Neither String nor Integer is more specific than the other // Solution: explicit cast demo.process((String) null); // "String: null" demo.process((Integer) null); // "Integer: null" demo.process((Object) null); // "Object: null" }} class NullWithHierarchy { void process(Object o) { System.out.println("Object"); } void process(String s) { System.out.println("String"); } // No Integer overload this time public static void main(String[] args) { NullWithHierarchy demo = new NullWithHierarchy(); demo.process(null); // Works! Prints "String" // String is more specific than Object, so no ambiguity }}Edge Case 2: Varargs and Arrays
Varargs methods can be called with explicit arrays, but this creates potential confusion:
123456789101112131415161718192021222324252627282930313233343536
class VarargsVsArrays { void process(int[] arr) { System.out.println("int[]: length " + arr.length); } void process(int... args) { System.out.println("varargs: length " + args.length); } // These two cannot coexist! int... is essentially int[] at runtime. // COMPILE ERROR: Duplicate method process(int[])} class VarargsEdgeCases { void log(Object... args) { System.out.println("Count: " + args.length); for (Object arg : args) { System.out.println(" - " + arg); } } public static void main(String[] args) { VarargsEdgeCases demo = new VarargsEdgeCases(); String[] strArray = {"a", "b", "c"}; demo.log(strArray); // Treated as single Object[]? Or 3 elements? // Prints: Count: 3, - a, - b, - c // The array is UNPACKED as varargs demo.log((Object) strArray); // Force single element // Prints: Count: 1, - [Ljava.lang.String;@... // The array is treated as one Object demo.log((Object[]) strArray); // Explicit: treat as 3 elements // Same as first call: Count: 3 }}Edge Case 3: Autoboxing and Widening Conflicts
1234567891011121314151617181920212223242526272829303132
class BoxingWidening { void test(Long l) { System.out.println("Long wrapper"); } void test(double d) { System.out.println("double primitive"); } public static void main(String[] args) { BoxingWidening demo = new BoxingWidening(); int value = 42; demo.test(value); // Which one? // Widening primitive (int → double) has priority over boxing // Prints: "double primitive" // To force boxing: demo.test(Long.valueOf(value)); // "Long wrapper" }} class NoWideningAfterBoxing { void accept(Long l) { System.out.println("Long"); } public static void main(String[] args) { NoWideningAfterBoxing demo = new NoWideningAfterBoxing(); int value = 42; // demo.accept(value); // COMPILE ERROR! // int cannot be boxed to Long - no boxing + widening for primitives // int → Integer → Long would require widening AFTER boxing // Java doesn't do that in a single conversion step demo.accept((long) value); // Works: widens then boxes }}Java will do widening OR boxing, but not both in a single step (except for the special case of widening after boxing to Object). If you need both, perform one conversion explicitly. When in doubt, be explicit with casts or wrapper constructors.
We've explored the detailed mechanics of overload resolution. Let's consolidate the essential rules:
What's Next:
Knowing the rules is essential, but knowing when to apply them is wisdom. The next page explores when to use method overloading—the design patterns where overloading shines and the scenarios where alternative approaches work better.
You now understand the precise mechanics of overload resolution: how the compiler identifies, ranks, and selects among overloaded methods. This knowledge helps you design clear overloads and debug resolution errors confidently. Next, we'll explore when overloading is the right design choice.