Method Overloading
Give several methods the same name with different parameters, know how the compiler picks one, and avoid the overloads that surprise callers.
String format(int value)
String format(long value)
String format(String value, int width)
// same name, different parameter listsOverloading means declaring several methods with the same name but different parameter lists: a different number of parameters, different types, or the same types in a different order. The JDK uses it everywhere: println has versions for int, long, char, String, Object and more, and List.of has twelve versions.
The return type does not count. Two methods named parse that both take a String will not compile, even if one returns int and the other long. Parameter names do not count either.
Standing in for default parameters
Java has no default parameter values. The usual replacement is a chain of overloads where the shorter ones call the longest one with defaults filled in. Keep the real logic in one place and let the others delegate to it, so a fix applies to every version. With more than two or three optional values, a builder or a record of options reads better than a pile of overloads.
How the compiler picks
The choice is made at compile time from the declared types of the arguments, not the runtime types of the objects. If a variable is declared as Object, the Object overload is chosen even when it holds a String. This is the big difference from overriding, which is decided at run time.
The compiler looks for a match in three passes, and stops at the first pass that finds one:
- Exact types and widening only (an
intcan go to alongparameter). - Boxing and unboxing allowed (
inttoInteger). - Varargs allowed.
Within a pass, the most specific method wins. If no single method is most specific, the call is ambiguous and does not compile. A common case is passing null when overloads take String and Integer; cast the null to pick one.
The overload that bites
List<Integer> has remove(int index) and remove(Object o). Calling ids.remove(1) removes the element at index 1, because pass one finds remove(int) before boxing is even tried. To remove the value 1, write ids.remove(Integer.valueOf(1)). With IDs from a database this mistake deletes the wrong row from a list, or throws IndexOutOfBoundsException.
Overloads that do different things are a design smell. If two methods behave differently, give them different names: findById and findByEmail are clearer than two find methods.
Example
When to use it
- Offer link(path), link(path, page) and link(path, page, size) to build pagination URLs with sensible defaults.
- Remove an ID by value from a List<Integer> of selected rows without accidentally removing by index.
- Provide toResponse(User) and toResponse(List<User>) mappers that turn domain objects into API responses.
More examples
List.remove(int) versus remove(Object)
remove(1) matches remove(int) in the first pass, before boxing is considered, so it removes by position. Boxing the argument yourself selects the remove(Object) overload.
void main() {
List<Integer> ids = new ArrayList<>(List.of(10, 20, 30));
ids.remove(1); // remove(int index): drops the element at index 1
IO.println(ids);
ids.remove(Integer.valueOf(30)); // remove(Object): drops the value 30
IO.println(ids);
boolean removed = ids.remove(Integer.valueOf(99));
IO.println("removed 99? " + removed);
}
// Output:
// [10, 30]
// [10]
// removed 99? falseOverloads that supply defaults
This is the standard way to fake default parameters in Java. Only the three-argument version builds the string, so changing the format happens in one place.
static final int DEFAULT_PAGE = 1;
static final int DEFAULT_SIZE = 20;
String link(String path) {
return link(path, DEFAULT_PAGE, DEFAULT_SIZE);
}
String link(String path, int page) {
return link(path, page, DEFAULT_SIZE);
}
// The one method that does the work; the others delegate here.
String link(String path, int page, int size) {
return path + "?page=" + page + "&size=" + size;
}
void main() {
IO.println(link("/users"));
IO.println(link("/users", 3));
IO.println(link("/users", 3, 50));
}
// Output:
// /users?page=1&size=20
// /users?page=3&size=20
// /users?page=3&size=50Widening beats boxing beats varargs
pick(5) never reaches the Integer overload because widening to long succeeds in the first pass, before boxing is tried. Overload sets like this compile but confuse readers, so avoid mixing primitive, boxed and varargs versions of one name.
String pick(long x) { return "long"; }
String pick(Integer x) { return "Integer"; }
String pick(int... xs) { return "varargs(" + xs.length + ")"; }
void main() {
IO.println(pick(5)); // pass 1: int widens to long
IO.println(pick(Integer.valueOf(5))); // pass 1: exact match on Integer
IO.println(pick()); // only varargs fits
IO.println(pick(1, 2)); // only varargs fits
}
// Output:
// long
// Integer
// varargs(0)
// varargs(2)
Discussion