Module 4 · Methods & Modularity ⏱ 18 min

Parameters, Returns, and Overloading

By the end of this lesson you will be able to:
  • Define a method's signature and explain why the return type is not part of it
  • Trace the compiler's overload-resolution ladder from exact match to widening
  • Predict which overload is selected, including the most-specific rule and ambiguity

You have written System.out.println(42) and System.out.println("hi") in the same program, and Java never once asked you to pick between printlnInt and printlnString. That single name accepting many argument types is not magic — it is method overloading, and it is one of the everyday conveniences that keeps Java readable. Without it, every type you ever wanted to print, log, or convert would need its own invented name, and your code would drown in near-identical helpers.

An overload is a second method that shares the first one's name but takes different parameters. This lesson is about predicting, with confidence, exactly which of several same-named methods the compiler will choose for a given call — because predicting wrong in your head is the difference between reading a stack trace and writing one.

flowchart LR
  S["signature = name + parameter types"] --> N["add(int, int)"]
  S --> N2["add(int, int, int)"]
  RT["return type is excluded"] -.-> S
  style S fill:#3776ab,color:#fff
  style RT fill:#b45309,color:#fff
A method's signature is its name plus its parameter types. The return type is not part of it.

What the compiler calls a signature

A method's signature is its name plus the ordered list of its parameter types — add(int, int) and add(int, int, int) are two different signatures, and add(int, double) is a third. The compiler uses exactly this to tell methods apart, which means two methods can share a name only when their parameter lists differ in type, order, or count.

Notice what the signature pointedly does not include: the return type. int add(int, int) and double add(int, int) have the same signature, so the compiler treats them as one method declared twice — a duplicate, not an overload. We will exploit that fact to read a common error in a moment.

Chosen at compile time, not runtime

Overload resolution is a compile-time decision. The compiler looks only at the declared types of your arguments, never at the values they happen to hold at runtime. Hand it a variable declared int and it routes to the int overload even if you know that, at runtime, the number could just as well have fit a double. There is no cleverness and no profiling — just types read straight off the source.

That is what makes overloading predictable. Once you can read the declared types at each call site, you can trace every route by hand, which is exactly the skill the exercises below sharpen.

Main.java — one name, three overloads. Each call finds its exact match. Real, compiling Java.
public class Main {
    static String describe(int n) {
        return "int " + n;
    }

    static String describe(double n) {
        return "double " + n;
    }

    static String describe(String s) {
        return "text " + s;
    }

    public static void main(String[] args) {
        System.out.println(describe(10));
        System.out.println(describe(2.5));
        System.out.println(describe("hi"));
    }
}
flowchart TD
  C["describe(arg) called"] --> Q{"exact type match available?"}
  Q -->|"yes"| EXACT["exact overload chosen"]
  Q -->|"no"| W{"widens to a parameter type?"}
  W -->|"yes"| WUSE["widening overload chosen"]
  W -->|"no"| B["boxing or varargs as last resort"]
  style EXACT fill:#15803d,color:#fff
  style WUSE fill:#3776ab,color:#fff
  style B fill:#b45309,color:#fff
The overload-resolution ladder: exact match first, then widening, then boxing and varargs as a last resort.

How the compiler chooses

When you call an overloaded name, the compiler does not guess. It walks a fixed ladder. First it hunts for an exact match — a parameter type identical to the argument's. Find one and the search ends. If no overload matches exactly, it tries widening: an int argument can satisfy a long or double parameter, because widening primitive conversions lose no information. Only if widening fails does it fall back to boxing (wrapping the primitive in its object type) and finally to varargs.

The widening chain runs byteshortintlongfloatdouble, always toward a wider type. Memorise that order and you can predict widening at a glance: an int can fill a long slot, never the reverse.

Main.java — only a long overload exists, so an int argument widens to it. Real, compiling Java.
public class Main {
    static String describe(long n) {
        return "long " + n;
    }

    public static void main(String[] args) {
        System.out.println(describe(5));    // 5 is int; no describe(int) exists
        System.out.println(describe(5L));   // 5L is long; exact match
    }
}
flowchart TD
  C["describe(5): both describe(long) and describe(double) apply"] --> Q{"is one overload more specific?"}
  Q -->|"yes: long widens to double"| PICK["describe(long) chosen"]
  Q -->|"no: neither dominates"| AMB["ambiguous, compile error"]
  style PICK fill:#15803d,color:#fff
  style AMB fill:#b91c1c,color:#fff
  style Q fill:#3776ab,color:#fff
Most specific wins: when two overloads both apply by widening, the one nearer the argument in the chain is chosen. A true tie is an error.

Most specific wins; ties are errors

Sometimes more than one overload is reachable by widening. Call describe(5) against describe(long) and describe(double) and both apply, because an int widens to either. The compiler does not give up — it picks the most specific one, meaning the overload whose parameter is closest to the argument in the widening chain. long sits before double, so long is more specific, and describe(long) wins.

When no overload is closer than another, the call is ambiguous and the build fails — the compiler refuses to guess on your behalf. Two methods greet(int, double) and greet(double, int) called as greet(5, 5) hit exactly this wall: each parameter favours a different overload, so neither dominates and the call is rejected.

Main.java — describe(5) is reachable by both overloads, but long is more specific than double. Real, compiling Java.
public class Main {
    static String describe(long n) {
        return "long " + n;
    }

    static String describe(double n) {
        return "double " + n;
    }

    public static void main(String[] args) {
        System.out.println(describe(5));
    }
}
Exercise

Exact match. Three overloads share the name describe. Predict all three lines — each argument finds a different overload.

public class Main {
    static String describe(int n) {
        return "int " + n;
    }

    static String describe(double n) {
        return "double " + n;
    }

    static String describe(String s) {
        return "text " + s;
    }

    public static void main(String[] args) {
        System.out.println(describe(10));
        System.out.println(describe(2.5));
        System.out.println(describe("hi"));
    }
}
Exercise

Widening. Only a long overload exists. Both calls print the same line — explain to yourself why the int argument is still accepted.

public class Main {
    static String describe(long n) {
        return "long " + n;
    }

    public static void main(String[] args) {
        System.out.println(describe(5));
        System.out.println(describe(5L));
    }
}
Exercise

Most specific. describe(5) is reachable by BOTH overloads through widening. Predict the single line — which one does the compiler pick?

public class Main {
    static String describe(long n) {
        return "long " + n;
    }

    static String describe(double n) {
        return "double " + n;
    }

    public static void main(String[] args) {
        System.out.println(describe(5));
    }
}
Exercise

A class declares these two methods. Why does it fail to compile?

static int score(int n)    { return n; }
static double score(int n) { return n; }
Exercise

Given these two overloads, what happens when you call greet(5, 5)?

static void greet(int a, double b) { ... }
static void greet(double a, int b) { ... }

Recap

  • A method's signature is its name plus its parameter types — the return type is excluded.
  • Overloading reuses one name across different parameter lists; the compiler selects by walking exact match → widening → boxing → varargs.
  • Widening follows the chain byteshortintlongfloatdouble.
  • When several overloads apply, the most specific one wins; when none is more specific, the call is ambiguous and fails to compile.
  • Resolution is a compile-time decision based on declared types only — no runtime values are consulted.

Next you will see where a method's variables live and die — scope, lifetime, and the surprises of shadowing.

Checkpoint quiz

What makes up a Java method's signature?

With m(int) and m(double) both defined, what does m(5) resolve to?

Go deeper — technical resources