Java Method Overloading Rules: Signatures and Compile-Time Resolution
Java method overloading rules decide which overloaded method runs. Learn what makes a valid signature, how compile-time resolution works, and common ambiguity traps.
When you define two methods with the same name in a Java class, the compiler must decide which one to invoke for a given call. That decision follows the Java method overloading rules defined by the Java Language Specification. Getting these rules wrong can lead to ambiguous calls, unexpected runtime behavior, or code that fails to compile. This article walks through the exact rules, the resolution order the compiler uses, and the edge cases that trip up even experienced developers.
The Core Rule: Same Name, Different Parameter List
Overloading allows multiple methods in the same class to share a name as long as their parameter lists differ. The difference can come from the number of parameters, the types of parameters, or the order of types. The method name alone is not enough to distinguish overloads; the compiler looks at the full signature, which includes the method name and the parameter types in order.
public class Calculator { public int add(int a, int b) { return a + b; } public int add(int a, int b, int c) { return a + b + c; } public double add(double a, double b) { return a + b; } }
These three add methods are valid overloads because the parameter lists differ: two ints, three ints, and two doubles. The return type is irrelevant for overloading, as explained later.
What Counts as a Different Parameter List
A parameter list is considered different if the types of the parameters, their count, or their order changes. Order matters when the types are distinct: method(String, int) and method(int, String) are different overloads because String and int appear in opposite order.
Type distinction is based on the declared type, not the actual runtime type of arguments. int and long are different types, so method(int) and method(long) are valid overloads. Similarly, String and Object are distinct, so method(String) and method(Object) can coexist.
Generic types also participate. method(List<String>) and method(List<Integer>) are the same signature after type erasure, so they cannot overload each other. This is a common source of confusion when working with generics.
Return Type and Throws Are Not Part of the Signature
The method signature used for overloading includes only the method name and the parameter types. The return type, the throws clause, and the access modifier are not part of the signature. You cannot overload a method by changing only the return type.
public class Example { public void process() { } public int process() { return 1; } // Compile error: duplicate method }
The compiler rejects this because both methods have the same signature process(). The return type is not part of the signature, so the methods cannot be distinguished by a call such as obj.process(). Similarly, changing the throws clause alone does not create a new overload.
How the Compiler Chooses an Overload
When you call an overloaded method, the compiler determines which version to invoke based on the compile-time types of the arguments. It does not look at the runtime type of the object or the arguments. This is called compile-time binding or static dispatch.
The compiler follows a three-phase selection process defined by the Java Language Specification:
- Phase 1: Strict invocation — no boxing, unboxing, or varargs. Identity, widening primitive, and widening reference conversions are considered.
- Phase 2: Loose invocation — boxing and unboxing are allowed, in addition to the conversions from Phase 1, but varargs are still excluded.
- Phase 3: Variable arity invocation — varargs methods are considered.
The compiler picks the most specific method among those applicable in the earliest phase. If no method is applicable, the call is a compile error. If multiple methods are equally specific, the call is ambiguous.
Widening, Boxing, and Varargs: The Priority Order
Understanding the priority order is critical for predicting which overload gets called. Consider this example:
public class OverloadDemo { public void print(int value) { System.out.println("int"); } public void print(long value) { System.out.println("long"); } public void print(Integer value) { System.out.println("Integer"); } public void print(int... values) { System.out.println("varargs"); } }
When you call print(5), the compiler first looks for a strict match. int matches int directly, so print(int) is chosen. If that method did not exist, the compiler would try widening to long before boxing to Integer or using varargs. For primitive arguments, the relevant order is:
- Phase 1: exact primitive match, then widening primitive conversion (for example,
inttolong). - Phase 2: boxing the primitive value (for example,
inttoInteger), or unboxing when the argument is a wrapper type. - Phase 3: varargs.
For a call with an int argument, the compiler prefers print(long) over print(Integer) because widening is considered before boxing. If print(long) did not exist, it would use print(Integer) before falling back to print(int...).
Widening reference conversion is also a strict conversion: method(String) is preferred over method(Object) for a String argument.
Common Ambiguity Scenarios
Ambiguity arises when the compiler cannot determine the most specific method. A common source is passing null to overloads that accept unrelated reference types:
public class Ambiguous { public void handle(String s) { } public void handle(Integer i) { } }
Calling handle(null) is ambiguous because null can be converted to both String and Integer, and neither type is more specific than the other.
By contrast, an overload set with handle(String) and handle(Object) is not ambiguous for handle(null) because String is more specific than Object:
public class NullOverload { public void handle(String s) { } public void handle(Object o) { } }
Varargs can also create ambiguity when two varargs methods have unrelated array types:
public class VarargAmbiguous { public void log(String... values) { } public void log(Integer... values) { } }
log(null) is ambiguous because null can be treated as either array type, and neither is more specific.
When mixing varargs with fixed arity, a fixed-arity method is preferred if it is applicable. For example, with method(int, int) and method(int...), a call with two int arguments invokes the fixed-arity version. Varargs is considered only when no applicable non-varargs method exists in an earlier phase.
Overloading and Inheritance: When Resolution Gets Tricky
Inheritance adds another layer. Overloaded methods can be defined in a superclass and subclass, but the resolution still happens at compile time based on the static type of the reference. This is different from overriding, where the runtime type determines the method.
class Parent { public void show(String s) { System.out.println("Parent String"); } } class Child extends Parent { public void show(Object o) { System.out.println("Child Object"); } }
If you have a Child reference and call child.show("hello"), the compiler sees both show(String) (inherited) and show(Object) (defined in Child). It picks show(String) because String is more specific than Object. This happens at compile time, regardless of the runtime type.
But if you assign the child to a Parent reference, parent.show("hello") only sees show(String) from the parent, so that is called. The overload resolution is based on the static type of the variable, not the actual object.
A common mistake is expecting runtime polymorphism to affect overloading. Overloading is static; overriding is dynamic. Mixing them can produce surprising results, especially when a subclass overrides one overload but not another.
Maintainability and Performance Considerations
Overloading is resolved at compile time, so there is no extra overload-resolution cost at runtime. The chosen signature is part of the compiled call. If the selected method is an instance method, normal virtual dispatch still applies when a subclass overrides it.
From a maintainability perspective, overloading can improve API readability when used consistently. For example, providing add(int, int) and add(double, double) lets callers use natural syntax without casting. But excessive overloading can make the API confusing, especially when the resolution rules are subtle. If callers cannot easily predict which overload runs, the API becomes error-prone.
One practical guideline is to avoid overloading methods that accept fundamentally different types but have the same semantic meaning, such as process(String) and process(File). These are better named differently (processString and processFile) to prevent accidental calls. When overloading is necessary, keep the parameter lists clearly distinct and document the resolution order.
Another concern is that adding a new overload can change the behavior of existing call sites without a compile error. For example, if an API currently has only method(Object), adding method(String) causes calls like method(null) to select the new String overload. Similarly, if only a varargs method existed, adding a fixed-arity overload can capture calls that previously fell through to varargs. When extending an overloaded API, consider whether a new, more specific overload will silently change existing callers' behavior.
Finally, consider the interaction with varargs. Varargs methods are often used as fallbacks, but they can introduce ambiguity and performance overhead because they can create an array for each call. If a varargs method is called frequently, the array allocation can add garbage. Prefer fixed-arity overloads when the number of arguments is known and limited.