Guide to Overloading Methods in Java

Introduction

Java defines a method as a unit of the tasks that a class can perform. And proper programming practice encourages us to ensure a method does one thing and one thing only.

It is also normal to have one method call another method when conducting a routine. Still, you expect these methods to have different identifiers to tell them apart. Or, to at least suggest what their internals do.

It is thus interesting when classes start offering methods with identical names - or rather, when they overload methods hence violating standards of clean code like the don't repeat yourself (DRY) principle.

Yet, as this article will show, methods featuring similar/same names are sometimes helpful. They can enhance the intuitiveness of API calls and with spare, smart use they can even enhance code readability.

What is Method Overloading?

Overloading is the act of defining multiple methods with identical names in the same class.

Still, to avoid ambiguity, Java demands that such methods have different signatures in order to be able to tell them apart.

It's important to remind ourselves of how to declare a method, to get a precise idea of how overloading occurs.

See, Java expects methods to feature up to six parts:

  1. Modifiers: e.g., public and private
  2. Return type: e.g., void, int, and String
  3. Valid method name/identifier
  4. Parameters (optional)
  5. Throwables (optional): e.g., IllegalArgumentException and IOException
  6. Method body

Thus a typical method may look like:

public void setDetails(String details) throws IllegalArgumentException {
    // Verify whether supplied details string is legal
    // Throw an exception if it's not
    // Otherwise, use that details string
}

The identifier and the parameters form the method signature or declaration.

For example, the method signature of the method above is - setDetails(String details).

Since Java can differentiate method signatures, it can afford method overloading.

Let's define a class with an overloaded method:

public class Address {
    public void setDetails(String details) {
        //...
    }
    public void setDetails(String street, String city) {
        //...
    }
    public void setDetails(String street, String city, int zipCode) {
        //...
    }
    public void setDetails(String street, String city, String zip) {
        //...
    }
    public void setDetails(String street, String city, String state, String zip) {
        //...
    }
}

Here, there's a method called setDetails() in several different forms. Some require just a String details, while some require a street, city, state, zip etc.

Calling the setDetails() method with a certain set of arguments will determine which method will be called. If no signature corresponds to your set of arguments, a compiler error will occur.

Why do we need Method Overloading?

Method overloading is useful in two primary scenarios. When you need a class to:

  • Create default values
  • Capture alternative argument types

Take the Address class below, for example:

public class Address {

    private String details;

    public Address() {
        this.details = String.format(
                "%s, %s \n%s, %s",      // Address display format
                new Object[] {          // Address details
                    "[Unknown Street]",
                    "[Unknown City]",
                    "[Unknown State]",
                    "[Unknown Zip]"});
    }

    // Getters and other setters omitted

    public void setDetails(String street, String city) {
        setDetails(street, city, "[Unknown Zip]");
    }

    public void setDetails(String street, String city, int zipCode) {
        // Convert the int zipcode to a string
        setDetails(street, city, Integer.toString(zipCode));
    }

    public void setDetails(String street, String city, String zip) {
        setDetails(street, city, "[Unknown State]", zip);
    }

    public void setDetails(String street, String city, String state, String zip) {
        setDetails(String.format(
            "%s \n%s, %s, %s",
            new Object[]{street, city, state, zip}));
    }

    public void setDetails(String details) {
        this.details = details;
    }

    @Override
    public String toString() {
        return details;
    }
}
Default Values

Say you only know an address's street and city, for instance. You would call the method setDetails() with two String parameters:

var address = new Address();
address.setDetails("400 Croft Road", "Sacramento");

And despite receiving a few details, the class will still generate a semblance of a full address. It will fill the missing details with default values.

So in effect, the overloaded methods have reduced the demands placed on clients. Users do not have to know an address in its entirety to use the class.

The methods also create a standard way of representing the class details in a readable form. This is especially convenient when one calls the class's toString():

400 Croft Road
Sacramento, [Unknown State], [Unknown Zip]

As the output above shows, a toString() call will always produce a value that is easy to interpret — devoid of nulls.

Alternative Argument Types

The Address class does not limit clients to providing the zip code in one data type alone. Besides accepting zip codes in String, it also handles those in int.

So, one may set Address details by calling either:

address.setDetails("400 Croft Road", "Sacramento", "95800");

or:

address.setDetails("400 Croft Road", "Sacramento", 95800);

Yet in both cases, a toString call on the class will output the following:

400 Croft Road
Sacramento, [Unknown State], 95800

Method Overloading vs the DRY Principle

Of course, method overloading introduces repetitions into a class. And it goes against the very core of what the DRY principle is all about.

The Address class, for example, has five methods that do somewhat the same thing. Yet on closer inspection, you will realize that that may not be the case. See, each of these methods handles a specific scenario.

  1. public void setDetails(String details) {}
  2. public void setDetails(String street, String city) {}
  3. public void setDetails(String street, String city, int zipCode) {}
  4. public void setDetails(String street, String city, String zip) {}
  5. public void setDetails(String street, String city, String state, String zip) {}

Whereas 1 allows a client to furnish an address without limitation to the format, 5 is quite strict.

In total, the five methods make the API extra friendly. They allow users to provide some of an address's details. Or all. Whichever a client finds convenient.

So, at the expense of DRY-ness, Address turns out to be more readable than when it has setters with distinct names.

Method Overloading in Java 8+

Before Java 8, we did not have lambdas, method references and such, so method overloading was a straightforward affair in some instances.

Say we have a class, AddressRepository, that manages a database of addresses:

public class AddressRepository {

    // We declare any empty observable list that
    // will contain objects of type Address
    private final ObservableList<Address> addresses
            = FXCollections.observableArrayList();

    // Return an unmodifiable collection of addresses
    public Collection<Address> getAddresses() {
        return FXCollections.unmodifiableObservableList(addresses);
    }

    // Delegate the addition of both list change and
    // invalidation listeners to this class
    public void addListener(ListChangeListener<? super Address> listener) {
        addresses.addListener(listener);
    }

    public void addListener(InvalidationListener listener) {
        addresses.addListener(listener);
    }

    // Listener removal, code omitted
}

If we wish to listen to changes in the address' list, we would attach a listener to the ObservableList, though in this example we've delegated this routine to AddressRepository.

We have, as a result, removed direct access to the modifiable ObservableList. See, such mitigation protects the address list from unsanctioned external operations.

Nonetheless, we need to track the addition and removal of addresses. So in a client class, we could add a listener by declaring:

var repository = new AddressRepository();
repository.addListener(listener -> {
    // Listener code omitted
});

Yet if you do this and compile, your compiler will throw the error:

reference to addListener is ambiguous
both method addListener(ListChangeListener<? super Address>) in AddressRepository and method addListener(InvalidationListener) in AddressRepository match

As a result, we have to include explicit declarations in the lambdas. We have to point to the exact overloaded method we are referring to. Hence, the recommended way to add such listeners in Java 8 and beyond is:

// We remove the Address element type from the
// change object for clarity
repository.addListener((Change<?> change) -> {
    // Listener code omitted
});

repository.addListener((Observable observable) -> {
    // Listener code omitted
});

In contrast, before Java 8, using the overloaded methods would have been unambiguous. When adding an InvalidationListener, for example, we would have used an anonymous class.

repository.addListener(new InvalidationListener() {
    @Override
    public void invalidated(Observable observable) {
        // Listener handling code omitted
    }
});

Best Practices

Excessive use of method overloading is a code smell.

Take a case where an API designer has made poor choices in parameter types while overloading. Such an approach would expose the API users to confusion.

This can, in turn, make their code susceptible to bugs. Also, the practice places excessive workloads on JVMs. They strain to resolve the exact types that poorly-designed method overloads refer to.

Yet, one of the most controversial uses of method overloading is when it features varargs, or to be formal, variable arity methods.

Remember, overloading usually telescopes the number of parameters that a client can supply so varargs introduce an extra layer of complexity. That is because they accommodate varying parameter counts — more on that in a second.

Limit varargs Usage in Overloaded Methods

There are many design decisions that revolve around how best to capture addresses. UI designers, for example, grapple with the order and number of fields to use to capture such details.

Programmers face a conundrum too - they have to consider the number of fixed variables an address' object needs, for instance.

A full definition of an address object could, for instance, have up to eight fields:

  1. House
  2. Entrance
  3. Apartment
  4. Street
  5. City
  6. State
  7. Zip
  8. Country

Yet, some UI designers insist that capturing these details in separate fields is not ideal. They claim that it increases the users' cognitive load. So, they usually suggest combining all the address details in a single text area.

As a result, the Address class in our case contains a setter that accepts one String parameter - details. Still, that in its own does not help the code's clarity. That is why we overloaded that method to cover several address fields.

But remember, varargs is an excellent way too to cater for varying parameter counts. We could thus simplify the code to a great extent by including a setter method like:

// Sets a String[]{} of details
public void setDetails(String... details) {
    // ...
}

We would have thus allowed the class' client to do something like:

// Set the house, entrance, apartment, and street
address.setDetails("18T", "3", "4C", "North Cromwell");

Yet, this poses a problem. Did the code above call this method:

public void setDetails(String line1, String line2, String state, String zip){
    // ...
}

Or, did it refer to:

public void setDetails(String... details) {
    // ...
}

In short, how should the code treat those details? Like specific address fields or like generalized details?

The compiler will not complain. It will not choose the variable arity method. What happens instead, is that the API designer creates ambiguity and this is a bug waiting to happen. Such as this:

address.setDetails();

The call above passes an empty String array (new String[]{}). While it is not technically erroneous, it doesn't solve any part of the domain problem. Thus, through varargs, the code has now become prone to bugs.

There is a hack to counter this problem, though. It involves creating a method from the method with the highest number of parameters.

In this case, using the method:

public void setDetails(String line1, String line2, String state, String zip) {
    // ...
}

To create:

public void setDetails(String line1, String line2, String state, String zip, String... other) {
    // ...
}

Still, the approach above is inelegant. Though error-free, it only increases the API's verbosity.

Be Aware of Autoboxing and Widening

Now let us assume we have a class, Phone, besides Address:

public class Phone {

    public static void setNumber(Integer number) {
        System.out.println("Set number of type Integer");
    }

    public static void setNumber(int number) {
        System.out.println("Set number of type int");
    }

    public static void setNumber(long number) {
        System.out.println("Set number of type long");
    }

    public static void setNumber(Object number) {
        System.out.println("Set number of type Object");
    }
}

If we call the method:

Phone.setNumber(123);

We will get the output:

Set number of type int

That is because the compiler chooses the overloaded method setNumber(int) first.

But, what if Phone did not have the method setNumber(int)? And we set 123 again? We get the output:

Set number of type long

setNumber(long) is the compiler’s second choice. In the absence of a method with the primitive int, the JVM foregoes autoboxing for widening. Remember, Oracle defines autoboxing as:

...the automatic conversion that the Java compiler makes between the primitive types and their corresponding object wrapper classes.

And widening as:

A specific conversion from type S to type T allows an expression of type S to be treated at compile time as if it had type T instead.

Next, let us remove the method setNumber(long) and set 123. Phone outputs:

Set number of type Integer

That is because the JVM autoboxes 123 into an Integer from int.

With the removal of setNumber(Integer) the class prints:

Set number of type Object

In essence, the JVM autoboxes and then widens the int 123 into an eventual Object.

Conclusion

Method overloading may improve code readability when you use it with care. In some instances, it even makes handling domain problems intuitive.

Nonetheless, overloading is a tricky tactic to master. Although it looks like something trivial to use — it is anything but. It forces programmers to consider the hierarchy of parameters types, for example - enter Java's autoboxing and widening facilities, and method overloading becomes a complex environment to work in.

Moreover, Java 8 introduced new features to the language, which compounded method overloads. Using functional interfaces in overloaded methods, for example, reduces an API's readability.

They force users to declare the parameter types in a client method. Thus, this defeats the entire purpose of method overloading — simplicity and intuitiveness.

You can find the code used in this article on GitHub.