Skip to content

VOL 01 / CH 05 / LESSON 04

5.4 Overload Resolution and API Design

Prerequisites: Method signatures, parameters, and return values

Estimated time: 20 minutes

You are ready to continue when: You can select an overload and judge whether an overload group expresses one coherent operation

The Compiler Starts with Argument Types

Overloads share a name and have different parameter lists. The compiler first looks for applicable methods without boxing or varargs packing, then considers boxing, and finally varargs calls. Within a phase, it must choose a most specific method; if no unique choice exists, the call is ambiguous.

java
public class OverloadChoice {
    static String pick(long value) { return "long"; }
    static String pick(Integer value) { return "Integer"; }
    static String describe(Object value) { return "object"; }
    static String describe(String value) { return "string"; }

    public static void main(String[] args) {
        Object text = "hello";
        System.out.println(pick(1));
        System.out.println(describe(text));
        System.out.println(describe("hello"));
    }
}

The output is long, object, string. Widening an int is considered before boxing it. The static type of text is Object; referring to a string does not change the selected overload to describe(String). Overloads with unrelated reference parameter types, such as print(String) and print(Integer), can make print(null) ambiguous.

Good Overloading Keeps the Same Semantics

java
static int max(int left, int right) {
    return left >= right ? left : right;
}

static long max(long left, long right) {
    return left >= right ? left : right;
}

Both methods represent "returning the larger of two values of the same type," although the numeric types differ.

Bad signal: A method with the same name saves a file, while another sends an email, simply because the parameters happen to differ. Callers see the name and cannot predict the side effects; these should be split into saveReport and emailReport.

Delegation Avoids Implementation Drift

java
static String describeItem(String name) {
    return describeItem(name, 1, false);
}

static String describeItem(String name, int level) {
    return describeItem(name, level, false);
}

static String describeItem(String name, int level, boolean rush) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("name is blank");
    }
    if (level < 1) {
        throw new IllegalArgumentException("level: " + level);
    }
    return name + ":" + level + ":" + rush;
}

Validation and core logic live in the most complete overload. Avoid having two overloads call each other and form a recursive loop.

Boolean Parameters Often Lack Readability

java
describeItem("widget", 3, true);

It's difficult for the caller to understand the meaning of true. Change the parameter type to an enum or options object. The proposed call below also requires implementing the corresponding method signature:

java
enum DeliverySpeed { STANDARD, RUSH }
java
describeItem("widget", 3, DeliverySpeed.RUSH);

Too Many Overloads Suggest a Parameter Object

When there are optional parameters such as name, level, rush, color, and engraving, overloading with all possible combinations can quickly explode in complexity. Use immutable request objects or a builder to express the combination and validate required parameters in one place.

There is no universal three-parameter limit. Judge the design by the number of combinations, the risk of misplacing parameters of the same type, the frequency of evolution, and the readability of calls.

Don't Let Overloads Sneakily Change Units

java
schedule(5);       // 5 Seconds, milliseconds, or days?
schedule(5L);      // Is it just the type changed, or the unit too?

If different overloads use different units, it's easy to accidentally choose the wrong method by selecting an integer literal. Use Duration and similar types with units, or make the unit explicit in the name.

Distinguishing Overloading from Overriding

  • Overloading: Methods with the same name but different parameters that are visible within the same scope. The compiler selects the appropriate method at compile time.
  • Overriding: A subclass provides an implementation for an instance method inherited from a superclass. The method chosen at runtime depends on the receiver object.

Both can occur together, making method calls more difficult to reason about. A complete example will be used in the object-oriented programming chapter to illustrate this.

Professional Reference

Design SignalRecommendation
Same concept, different natural input shapesOverloadable
Side effects or different semanticsUse different method names
Default valuesShort overload delegates full overload
Multiple boolean or same-type parametersEnum, value object, or request object
Different unitsUse a type with units, avoid guessing by overload
Combined exponential growthBuilder or parameter object

Before Moving On

Check each overloaded group item by item: Are all entries expressing the same concept? Are default values defined in only one place? Do boolean parameters and units lead callers to guess? Should a parameter object be used when combinations continue to grow? Overloading is selected at compile time, and overriding is dispatched at runtime based on the receiver. Do not conflate the two.

Next: Call Stack, Stack Frames, and Stack Traces

Specification: JLS §15.12.2.

Built with VitePress | Software Systems Atlas