Method Overloading

Give several methods the same name with different parameters, know how the compiler picks one, and avoid the overloads that surprise callers.

SyntaxString format(int value) String format(long value) String format(String value, int width) // same name, different parameter lists

Overloading 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:

  1. Exact types and widening only (an int can go to a long parameter).
  2. Boxing and unboxing allowed (int to Integer).
  3. 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · java
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? false

Overloads 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.

Example · java
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=50

Widening 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.

Example · java
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

  • Be the first to comment on this lesson.