JAVA / METHODS
Overloading one name with many signatures
Read a set of same-named Java methods, predict which one any given call selects, and design overload sets that stay unambiguous.
What you will learn
- Distinguish overloads by parameter count, types, or order, not by return type.
- Trace resolution: name and arity, then applicability, then most specific match.
- Predict that widening beats boxing and boxing beats varargs when picking overloads.
- Funnel short overloads into one full method so the logic lives in a single place.
Understanding Overloading one name with many signatures
Overloading means declaring several methods in one class that share a name but differ in their parameter lists: in how many parameters they take, in the types of those parameters, or in the order of those types. To the compiler these are unrelated methods that merely happen to be spelled alike; it builds a signature for each from the name and the parameter types only. Parameter names and the return type are not part of that signature, so an int size() sitting next to a long size() is rejected with 'method size() is already defined in class', while tag(int) next to tag(long) is perfectly legal.
Picking one of them is a compile-time search over the declared types of the arguments, and it runs in three passes. The compiler gathers every method with the matching name and a workable arity, then looks for an applicable candidate using only subtyping and primitive widening; if none is found it retries allowing boxing and unboxing, and only after that allowing varargs. When a pass yields several applicable candidates the most specific one wins, meaning the one whose parameter types could themselves be passed to all the rivals. That pass order is exactly why a call tag(12) prefers tag(long) over tag(Integer): widening int to long is a first-pass conversion, boxing int to Integer is not.
Because the search reads declared types, the object actually stored in the variable at run time has no influence. Given Object hidden = someString, the call tag(hidden) enters the Object overload even though the value is a String; the run-time, value-driven dispatch you may be picturing is overriding, a different mechanism that needs identical signatures on an instance method. The design rule follows from this: overloads should be interchangeable ways of describing one operation, so that it barely matters which one the compiler chose. When two same-named methods would genuinely behave differently, give them different names instead.
public class Overloading {
static String tag(int n) {
return "int:" + n;
}
static String tag(long n) {
return "long:" + n;
}
static String tag(double n) {
return "double:" + n;
}
static String tag(Object o) {
return "Object:" + o;
}
static String tag(String s) {
return "String:" + s;
}
public static void main(String[] args) {
short small = 12;
System.out.println(tag(small)); // short widens to int, the nearest fit
System.out.println(tag(12L)); // exact match on long
System.out.println(tag(12.5f)); // float can only widen to double
System.out.println(tag("hi")); // String is more specific than Object
Object hidden = "hi";
System.out.println(tag(hidden)); // the declared type decides, not the value
Integer boxed = 12;
System.out.println(tag(boxed)); // Object matches without unboxing
}
}Overload selection is made by the compiler from the declared types of the arguments, so the shared name is resolved to one fixed method before the program ever runs.
Worked examples
Short overloads that fill in defaults
Overloads differing only in arity delegate to one full method so the behaviour cannot drift apart.
public class Joins {
static String join(String[] parts, String sep, String prefix) {
StringBuilder sb = new StringBuilder(prefix);
for (int i = 0; i < parts.length; i++) {
if (i > 0) {
sb.append(sep);
}
sb.append(parts[i]);
}
return sb.toString();
}
static String join(String[] parts, String sep) {
return join(parts, sep, "");
}
static String join(String[] parts) {
return join(parts, ", ");
}
public static void main(String[] args) {
String[] names = { "ada", "linus", "grace" };
System.out.println(join(names));
System.out.println(join(names, " | "));
System.out.println(join(names, "-", "> "));
}
}Example explained
Line 1join(String[]) has no separator of its own, so it forwards with ", " supplied and adds nothing else.
Line 2Only the three-parameter version touches the StringBuilder, so correcting the loop corrects all three entry points at once.
Line 3These three differ in argument count, so the compiler separates them by arity alone and no conversion rules come into play.
Line 4The prefix is appended once before the loop, which is why only the third line starts with the marker.
Casting to steer the choice
A cast changes the declared type of the argument and therefore changes which overload the compiler selects.
public class Choose {
static String show(String s) {
return "String overload, s=" + s;
}
static String show(Object o) {
return "Object overload, o=" + o;
}
public static void main(String[] args) {
System.out.println(show((String) null));
System.out.println(show((Object) null));
System.out.println(show("text"));
}
}Example explained
Line 1(String) null gives the argument the declared type String, so show(String) is applicable and is the more specific of the two.
Line 2(Object) null hides String from the compiler, leaving show(Object) as the only applicable candidate.
Line 3A bare show(null) would still compile here because String is a subtype of Object; it breaks as soon as a second, unrelated reference overload exists.
Line 4Both null calls still print the text null, because string concatenation renders a null reference that way.
The List.remove trap
The same call name deletes by position for an int and by value for an Integer, purely because of overload resolution.
import java.util.ArrayList;
import java.util.List;
public class RemoveOverloads {
public static void main(String[] args) {
List<Integer> nums = new ArrayList<>();
nums.add(10);
nums.add(20);
nums.add(30);
nums.remove(1);
System.out.println(nums);
nums.remove(Integer.valueOf(30));
System.out.println(nums);
}
}Example explained
Line 1remove(1) matches remove(int index) exactly in the first pass, so position 1 is removed and the value 20 vanishes.
Line 2Integer.valueOf(30) has declared type Integer, which reaches remove(Object) by a reference widening, so unboxing is never attempted.
Line 3Neither call is ambiguous, which is what makes the bug quiet: the compiler is satisfied and the wrong element goes.
Important notes
A call like f(null) compiles only when one applicable parameter type is a subtype of all the others; with unrelated types such as String and StringBuilder javac reports that the reference to f is ambiguous and you must cast.
Renaming a parameter, or adding final to it, does not create an overload, and neither do differing generic arguments: List<String> and List<Integer> both erase to f(List), so the two declarations clash.
Common mistakes
Trying to overload on the return type, as in int parse(String) beside double parse(String); the class will not compile at all, because the return type is not part of the signature the compiler compares.
Assuming the value decides: with Object o = "hi", a call to describe(o) runs the Object overload and the String-specific formatting is skipped silently, with no warning and no error.
Calling list.remove(2) on a List<Integer> expecting the value 2 to go; the int overload removes the element at index 2 instead, or throws IndexOutOfBoundsException when the list is shorter than that.
Try it yourself
Change, predict, then run
Declare three static methods named label taking int, Integer and Object, each printing which parameter type it received, then call label with the literal 5, with Integer.valueOf(5), and with an Object variable holding 5. Write down your prediction for all three lines before running it.
Open the Java workspaceCheck your understanding
A class declares f(long x) and f(Integer x). What happens for the call f(7), and why?
- Integer runs, because an int literal is boxed to its wrapper before any other conversion is considered
- long runs, because the first resolution pass permits primitive widening but not boxing
- Neither runs, because the call is ambiguous and the code does not compile
- Integer runs, because f(Integer) has the more specific parameter type and the most specific candidate wins
Show answer
The compiler's first pass considers only subtyping and primitive widening; int to long is a widening primitive conversion, so f(long) is applicable there and the search stops without ever reaching the boxing pass. The last option is tempting because specificity really does break ties, but specificity is only compared among candidates that are applicable in the pass currently being run, and f(Integer) does not qualify in the first one.