AI InterviewSearch…Search problems, topics, or pages…Ctrl KGet started
Skip to content

05 Methods / Lesson 3 of 6

Overloading and how the compiler picks

14 min · +10 XP · 2 exercises · 5 questions

Read the text14 min
Exercises0/2
Quiz5 q

Quick answer

What is method overloading in Java?

Method overloading in Java means declaring several methods with the same name but different parameter lists, such as max(int, int) and max(double, double). The compiler picks one at compile time from the declared types of the arguments, preferring widening over boxing and boxing over varargs. Overloads cannot differ only in their return type.

Two methods with the same name and different parameter lists are overloads. System.out.println has ten of them; Math.max has four; you will write your own to offer convenient variants of one operation. The compiler chooses among them at compile time by a three-phase rule that is precise, occasionally surprising, and a favourite interview topic.

What can be overloaded

static int    max(int a, int b)
static long   max(long a, long b)
static double max(double a, double b)
static int    max(int a, int b, int c)

Overloads differ in the number or types of parameters (the signature). They may not differ only in return type — int f() and long f() in one class is an error, because a call f() could not choose. Parameter names are irrelevant. Overloads may have different return types, modifiers and exceptions, as long as the parameter lists differ.

Overloading is resolved statically: the compiler looks at the declared types of the argument expressions, not what they hold at run time. That is the crucial difference from overriding (Module 8), which is resolved at run time by the receiver's actual class.

The three phases

Given a call, the compiler finds the applicable overloads in three phases and stops at the first phase that finds any:

  1. Exact match or primitive widening / reference subtyping — no boxing, no varargs.
  2. With boxing/unboxing allowed.
  3. With varargs allowed.

Within a phase, the most specific method wins (an int parameter is more specific than long, String more specific than Object). If two are equally specific, the call is ambiguous and fails to compile.

static void f(long x)      { System.out.println("long"); }
static void f(Integer x)   { System.out.println("Integer"); }
static void f(Object x)    { System.out.println("Object"); }
static void f(int... x)    { System.out.println("varargs"); }

f(5);       // "long"    — phase 1: int widens to long; boxing (Integer) is phase 2

Widening beats boxing. Remove f(long) and the answer becomes "Integer" (phase 2, boxing, and Integer is more specific than Object). Remove that too and it is "Object" (still phase 2: box to Integer, then subtype to Object). Only with all three gone does varargs win.

Widening preferences

Among primitives, the compiler picks the smallest widening: with f(long), f(float) and f(double) available, f(5) calls f(long). char widens to int before anything else: f('a') with f(int) and f(long) calls f(int).

byte/short/char never narrow to match: f((short) 1) cannot call f(byte).

Reference types: most specific wins

static void g(Object o)  { … "Object" }
static void g(String s)  { … "String" }

g("hi");            // "String" — more specific
g(null);            // "String" — null converts to any reference; String is more specific than Object
Object o = "hi";
g(o);               // "Object" — the DECLARED type of o is Object; resolution is static

The last line is the one to remember: the argument's static type decides, not the object inside. And g(null) with g(String) and g(Integer) is ambiguous — neither is more specific than the other — and fails to compile; cast the null: g((String) null).

Boxing pitfalls

static void h(long x)   { … }
static void h(Integer x){ … }
Integer boxed = 5;
h(boxed);           // "Integer" — exact match in phase 1 (no conversion needed)
h(5);               // "long"    — widening (phase 1) beats boxing (phase 2)

A related classic: List<Integer> list; list.remove(1) calls remove(int index) — removing the element at index 1 — not remove(Object o), because the exact primitive match wins. To remove the value 1: list.remove(Integer.valueOf(1)).

Overloading and null

System.out.println(null) is ambiguous (char[] and String both apply). println((Object) null) compiles. Passing bare null to an overloaded method is a smell for this reason.

Overloading versus defaults

Java has no default parameter values. Overloads are the way to offer defaults: the short version calls the full one.

void connect(String host)            { connect(host, 80, 30_000); }
void connect(String host, int port)  { connect(host, port, 30_000); }
void connect(String host, int port, int timeoutMs) { … the real work … }

One implementation, several entry points. This "telescoping" pattern is standard; beyond three or four parameters, a builder (Module 7) reads better.

Overloads and constructors

Constructors overload the same way, and this(...) chains them (Module 7). The same resolution rules apply to new Foo(5).

Guidelines

  • Overloads should do the same thing for different inputs. add(int) and add(String) doing unrelated work is a naming failure.
  • Avoid overloads whose parameters differ only by boxing or widening (f(int) vs f(Integer), f(long) vs f(int)) — callers cannot predict which they get. Math.max's four are fine because they behave identically.
  • Avoid overloads that both accept null-able reference types at the same position.
  • Never rely on varargs to disambiguate.
  • @Override-style safety does not exist for overloads: a typo in a parameter type silently creates a new overload instead of an error. Tests catch it; the compiler does not.

Interview angle

  • "Can methods be overloaded by return type?" No.
  • "f(5) with f(long) and f(Integer) — which runs?" f(long): widening before boxing.
  • "Is overloading resolved at compile time or run time?" Compile time, from the arguments' declared types.
  • "list.remove(1) on a List<Integer>?" Removes index 1 — exact match on int.
  • "What does g(null) do with g(String) and g(Object)?" Calls g(String), the more specific.

Key takeaways

  • Overloads differ in parameter lists; return type alone does not count.
  • Resolution is static (declared types) in three phases: widening/subtyping → boxing → varargs; most specific wins; ties are errors.
  • Widening beats boxing beats varargs; String beats Object; null picks the most specific reference overload or is ambiguous.
  • Use overloads for defaults and same-behaviour variants; avoid boxing-only differences.

Common questions

Can we overload a method by changing only the return type in Java?

No. Overloads must differ in the number or types of their parameters; int f() and long f() in one class is a compile error, because a call f() gives the compiler no way to choose. Overloads may still have different return types when their parameter lists differ.

What is the difference between overloading and overriding in Java?

Overloading is several methods with the same name and different parameters, chosen at compile time from the arguments' declared types. Overriding is a subclass redefining an inherited method with the same signature, chosen at run time from the object's actual class.

Which overload does f(5) call when f(long) and f(Integer) both exist?

f(long). Overload resolution runs in three phases, first without boxing, then with boxing, then with varargs, and stops at the first phase that finds a match; widening int to long is allowed in phase one, while boxing to Integer needs phase two.

Why does list.remove(1) remove the element at index 1?

On a List<Integer>, remove(1) matches remove(int index) exactly, which beats remove(Object o) because that would need boxing. To remove the value 1 instead, call list.remove(Integer.valueOf(1)).

Does Java have default parameter values?

No. Java offers defaults through overloads that fill in the missing values and call the full version: connect(host) calls connect(host, 80, 30_000). Beyond three or four parameters, a builder usually reads better.

Mark as readSign in to keep your place.

Method Overloading in Java: Widening, Boxing and Varargs

Method overloading in Java: methods share a name but differ in parameters. The compiler picks one at compile time, widening before boxing before varargs.

  • Course: Java study plan
  • Module: Methods
  • Kind: Lesson
  • Reading time: 14 min
  • Runtime: OpenJDK 18

What is method overloading in Java?

Method overloading in Java means declaring several methods with the same name but different parameter lists, such as max(int, int) and max(double, double). The compiler picks one at compile time from the declared types of the arguments, preferring widening over boxing and boxing over varargs. Overloads cannot differ only in their return type.

Lesson

Two methods with the same name and different parameter lists are overloads. System.out.println has ten of them; Math.max has four; you will write your own to offer convenient variants of one operation. The compiler chooses among them at compile time by a three-phase rule that is precise, occasionally surprising, and a favourite interview topic.

What can be overloaded

static int    max(int a, int b)
static long   max(long a, long b)
static double max(double a, double b)
static int    max(int a, int b, int c)

Overloads differ in the number or types of parameters (the signature). They may not differ only in return type — int f() and long f() in one class is an error, because a call f() could not choose. Parameter names are irrelevant. Overloads may have different return types, modifiers and exceptions, as long as the parameter lists differ.

Overloading is resolved statically: the compiler looks at the declared types of the argument expressions, not what they hold at run time. That is the crucial difference from overriding (Module 8), which is resolved at run time by the receiver's actual class.

The three phases

Given a call, the compiler finds the applicable overloads in three phases and stops at the first phase that finds any:

  1. Exact match or primitive widening / reference subtyping — no boxing, no varargs.
  2. With boxing/unboxing allowed.
  3. With varargs allowed.

Within a phase, the most specific method wins (an int parameter is more specific than long, String more specific than Object). If two are equally specific, the call is ambiguous and fails to compile.

static void f(long x)      { System.out.println("long"); }
static void f(Integer x)   { System.out.println("Integer"); }
static void f(Object x)    { System.out.println("Object"); }
static void f(int... x)    { System.out.println("varargs"); }

f(5);       // "long"    — phase 1: int widens to long; boxing (Integer) is phase 2

Widening beats boxing. Remove f(long) and the answer becomes "Integer" (phase 2, boxing, and Integer is more specific than Object). Remove that too and it is "Object" (still phase 2: box to Integer, then subtype to Object). Only with all three gone does varargs win.

Widening preferences

Among primitives, the compiler picks the smallest widening: with f(long), f(float) and f(double) available, f(5) calls f(long). char widens to int before anything else: f('a') with f(int) and f(long) calls f(int).

byte/short/char never narrow to match: f((short) 1) cannot call f(byte).

Reference types: most specific wins

static void g(Object o)  { … "Object" }
static void g(String s)  { … "String" }

g("hi");            // "String" — more specific
g(null);            // "String" — null converts to any reference; String is more specific than Object
Object o = "hi";
g(o);               // "Object" — the DECLARED type of o is Object; resolution is static

The last line is the one to remember: the argument's static type decides, not the object inside. And g(null) with g(String) and g(Integer) is ambiguous — neither is more specific than the other — and fails to compile; cast the null: g((String) null).

Boxing pitfalls

static void h(long x)   { … }
static void h(Integer x){ … }
Integer boxed = 5;
h(boxed);           // "Integer" — exact match in phase 1 (no conversion needed)
h(5);               // "long"    — widening (phase 1) beats boxing (phase 2)

A related classic: List<Integer> list; list.remove(1) calls remove(int index) — removing the element at index 1 — not remove(Object o), because the exact primitive match wins. To remove the value 1: list.remove(Integer.valueOf(1)).

Overloading and null

System.out.println(null) is ambiguous (char[] and String both apply). println((Object) null) compiles. Passing bare null to an overloaded method is a smell for this reason.

Overloading versus defaults

Java has no default parameter values. Overloads are the way to offer defaults: the short version calls the full one.

void connect(String host)            { connect(host, 80, 30_000); }
void connect(String host, int port)  { connect(host, port, 30_000); }
void connect(String host, int port, int timeoutMs) { … the real work … }

One implementation, several entry points. This "telescoping" pattern is standard; beyond three or four parameters, a builder (Module 7) reads better.

Overloads and constructors

Constructors overload the same way, and this(...) chains them (Module 7). The same resolution rules apply to new Foo(5).

Guidelines

  • Overloads should do the same thing for different inputs. add(int) and add(String) doing unrelated work is a naming failure.
  • Avoid overloads whose parameters differ only by boxing or widening (f(int) vs f(Integer), f(long) vs f(int)) — callers cannot predict which they get. Math.max's four are fine because they behave identically.
  • Avoid overloads that both accept null-able reference types at the same position.
  • Never rely on varargs to disambiguate.
  • @Override-style safety does not exist for overloads: a typo in a parameter type silently creates a new overload instead of an error. Tests catch it; the compiler does not.

Interview angle

  • "Can methods be overloaded by return type?" No.
  • "f(5) with f(long) and f(Integer) — which runs?" f(long): widening before boxing.
  • "Is overloading resolved at compile time or run time?" Compile time, from the arguments' declared types.
  • "list.remove(1) on a List<Integer>?" Removes index 1 — exact match on int.
  • "What does g(null) do with g(String) and g(Object)?" Calls g(String), the more specific.

Key takeaways

  • Overloads differ in parameter lists; return type alone does not count.
  • Resolution is static (declared types) in three phases: widening/subtyping → boxing → varargs; most specific wins; ties are errors.
  • Widening beats boxing beats varargs; String beats Object; null picks the most specific reference overload or is ambiguous.
  • Use overloads for defaults and same-behaviour variants; avoid boxing-only differences.

Common questions

Can we overload a method by changing only the return type in Java?

No. Overloads must differ in the number or types of their parameters; int f() and long f() in one class is a compile error, because a call f() gives the compiler no way to choose. Overloads may still have different return types when their parameter lists differ.

What is the difference between overloading and overriding in Java?

Overloading is several methods with the same name and different parameters, chosen at compile time from the arguments' declared types. Overriding is a subclass redefining an inherited method with the same signature, chosen at run time from the object's actual class.

Which overload does f(5) call when f(long) and f(Integer) both exist?

f(long). Overload resolution runs in three phases, first without boxing, then with boxing, then with varargs, and stops at the first phase that finds a match; widening int to long is allowed in phase one, while boxing to Integer needs phase two.

Why does list.remove(1) remove the element at index 1?

On a List<Integer>, remove(1) matches remove(int index) exactly, which beats remove(Object o) because that would need boxing. To remove the value 1 instead, call list.remove(Integer.valueOf(1)).

Does Java have default parameter values?

No. Java offers defaults through overloads that fill in the missing values and call the full version: connect(host) calls connect(host, 80, 30_000). Beyond three or four parameters, a builder usually reads better.

Exercises

Area overloads

Write three overloads of area: area(int side) for a square, area(int w, int h) for a rectangle, and area(double radius) for a circle (Math.PI * r * r). Read an integer n, then n lines of the form square 3, rect 3 4 or circle 2.5, call the matching overload, and print each area with two decimals.

Note that area(3) must call the int version and area(2.5) the double one — parse the arguments with the right types.

Example: square 3 → 9.00; circle 1 → 3.14.

Which overload?

Define four overloads named describe: describe(int), describe(long), describe(double) and describe(Object), each returning its own name ("int", "long", "double", "Object"). Read n tokens of the form <kind>:<value> where kind is int, long, double, char, short, Integer or String; declare a variable of exactly that type, call describe with it, and print <token> -> <chosen overload>. Widening beats boxing; a char or short widens to int; an Integer is an Object.

Example: 4 then int:5 char:a Integer:5 short:7 →

int:5 -> int
char:a -> int
Integer:5 -> Object
short:7 -> int

In this module: Methods

← Java is pass-by-value — always · Varargs — methods that take any number of arguments →