Skip to content
Part IA Michaelmas Term

Method Overloading

What Overloading Is

Overloading allows multiple methods in the same class (or a class and its superclass) to share the same name, provided their parameter signatures differ. The compiler resolves which method to call based on the number, types, and order of arguments at the call site.

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

All three methods are named add, but they have different parameter lists: two ints, two doubles, and three ints. The compiler knows which one to call from the arguments provided.

The Rules

Overload resolution is determined solely by:

  1. The number of parameters (arity)
  2. The types of parameters
  3. The order of parameter types

The method’s return type is not part of the signature for overload resolution. Neither are parameter names, access modifiers, or thrown exceptions.

Return Type Is NOT Sufficient

public int getValue() { return 42; }
public double getValue() { return 42.0; }

This is a compile error: getValue() is already defined. The compiler cannot distinguish these two methods because they have identical parameter lists (both have zero parameters). If the return type were part of the signature, the compiler would not know which method you wanted at the call site:

var x = getValue();  // Which getValue? Ambiguous.

How the Compiler Resolves Overloads

When the compiler encounters a method call, it searches for the most specific applicable overload:

  1. Find all methods with the correct name
  2. Discard those whose parameter count does not match
  3. For each remaining candidate, check whether the arguments can be converted to the parameter types
  4. Choose the most specific match — in cases of ambiguity, prefer the overload that does not require a widening conversion
public void foo(int x) { ... }
public void foo(double x) { ... }

foo(42);    // calls foo(int) — exact match
foo(3.14);  // calls foo(double) — exact match
foo(42L);   // calls foo(double) — long widens to double, not to int

The long argument 42L cannot narrow to int implicitly, so it widens to double and calls foo(double).

Constructor Overloading

Overloading is especially common with constructors. A class can provide multiple ways to create an instance:

public class Point {
    private double x;
    private double y;

    public Point() {
        this(0.0, 0.0);
    }

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public Point(Point other) {
        this(other.x, other.y);
    }
}

Three constructors, all named Point, distinguished by their parameters. The first constructor delegates to the second using this(0.0, 0.0) — more on this in the constructor overloading notes.

Overloading vs Overriding

This distinction is a frequent Tripos topic:

OverloadingOverriding
When resolvedCompile time (static dispatch)Runtime (dynamic dispatch)
WhereSame class, or class + superclassSubclass replacing superclass method
SignatureSame name, different parameter typesSame name, same parameter types
Return typeCan differ, but cannot be the sole differenceMust be same or covariant
AnnotationNone needed@Override recommended

Consider:

class Animal {
    public void speak() { System.out.println("..."); }
}

class Dog extends Animal {
    @Override
    public void speak() { System.out.println("Woof"); }   // overriding

    public void speak(int times) {                          // overloading
        for (int i = 0; i < times; i++) {
            speak();
        }
    }
}

Dog.speak() overrides Animal.speak() — same signature. Dog.speak(int) overloads it — different parameter list.

No Standalone Functions

Java has no standalone functions like C or Python. Every method must belong to a class (or interface). Even main is a static method of some class. Utility functions go in classes like Math or Collections as static methods.

This reinforces the object-oriented principle that behaviour is always associated with a type. In practice, it means you will sometimes create “utility classes” — classes with only static methods that exist solely to hold functions — which is a pragmatic necessity but arguably a departure from pure OOP.

Common Traps

Ambiguous Overloads

public void process(int x, double y) { ... }
public void process(double x, int y) { ... }

obj.process(5, 10);   // COMPILE ERROR: ambiguous — both 5 and 10 are ints,
                       // and either could widen to double

Both overloads are equally specific, and neither is unambiguously the best match.

Autoboxing and Overloading

public void handle(Integer x) { ... }
public void handle(int x) { ... }

handle(42);   // calls handle(int) — prefers primitive over boxing

The compiler prefers the primitive overload when both are applicable, because the exact match (no conversion at all) is more specific.

Varargs and Overloading

public void log(String msg) { ... }
public void log(String... msgs) { ... }

log("hello");   // calls log(String) — prefers non-varargs overload

The compiler prefers a fixed-arity overload over a varargs overload when both are applicable.