Method Overload
Method overloading means declaring several methods with the same name but different parameter lists in the same class (or across a class and its subclasses). Callers use one meaningful name, and the compiler picks the right version from the arguments. System.out.println is the most familiar example: one name, with versions for int, double, char, boolean, String, Object, and more.
Basic Example
public class AreaCalculator {
static double area(double radius) { // circle
return Math.PI * radius * radius;
}
static double area(double width, double height) { // rectangle
return width * height;
}
static double area(double a, double b, double c) { // triangle, by its three sides
double s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
public static void main(String[] args) {
System.out.printf("%.2f%n", area(2)); // 12.57
System.out.printf("%.2f%n", area(4, 5)); // 20.00
System.out.printf("%.2f%n", area(3, 4, 5)); // 6.00
}
}
All three methods answer the same question, "what is the area?", for different inputs. Without overloading they would need names like circleArea, rectangleArea, and triangleArea. Overloading is appropriate exactly when the versions perform the same conceptual operation.
What Makes Overloads Different
Two methods are overloads if they have the same name and different signatures. A method's signature is its name plus its parameter types, in order:
| Difference | Valid overloads |
|---|---|
| Number of parameters | max(int, int) and max(int, int, int) |
| Parameter types | parse(String) and parse(char[]) |
| Order of differing types | log(String, int) and log(int, String) |
What does not count:
int total(int a, int b) { ... }
double total(int a, int b) { ... } // compile error: return type alone does not distinguish
void save(String name) { ... }
void save(String title) { ... } // compile error: parameter names do not matter
void run(int x) throws IOException { ... }
void run(int x) { ... } // compile error: throws clause does not matter
The return type cannot distinguish overloads because a call such as total(1, 2); whose result is discarded gives the compiler no way to choose.
How the Compiler Chooses an Overload
The choice happens at compile time, using the declared types of the arguments. When there is no exact match, the compiler considers conversions in three phases, stopping at the first phase that finds a match:
- Exact match or widening primitive conversion (
int→long→float→double), and subtype-to-supertype for references. - Boxing or unboxing (
int↔Integer). - Varargs.
Within a phase, if several methods match, it picks the most specific one: the method whose parameter types are the narrowest.
static void show(long x) { System.out.println("long"); }
static void show(Integer x) { System.out.println("Integer"); }
static void show(Object x) { System.out.println("Object"); }
static void show(int... x) { System.out.println("varargs"); }
show(5); // long (phase 1: widening beats boxing)
show(Integer.valueOf(5));// Integer (phase 1: exact reference match)
show("text"); // Object (phase 1: String is a subtype of Object)
show(); // varargs (only varargs accepts zero arguments)
The first result surprises many people: an int argument chose long over Integer. Widening was part of Java before autoboxing existed, and the phases preserve the old behavior so existing code did not change meaning.
Most specific wins
static void handle(Object o) { System.out.println("Object"); }
static void handle(String s) { System.out.println("String"); }
handle("hello"); // String: both match, String is more specific
handle(null); // String: null fits both; String is more specific
Ambiguity
If no single method is most specific, the call does not compile:
static void pick(Integer a, long b) { }
static void pick(long a, Integer b) { }
pick(1, 2); // compile error: reference to pick is ambiguous
static void find(String s) { }
static void find(Integer i) { }
find(null); // compile error: ambiguous; neither is more specific
find((String) null); // fine: the cast selects the version
Overloading Is Resolved by Declared Type
Because the choice is made at compile time, the declared type of an argument matters, not its runtime type:
static void describe(Object o) { System.out.println("an object"); }
static void describe(String s) { System.out.println("a string"); }
Object value = "hello";
describe(value); // "an object": value is declared as Object
describe((String) value);// "a string"
This is the key difference from method overriding, where the runtime type of the object selects the method. Overloading never looks at the runtime type of the arguments.
Common Uses
Convenience versions with defaults
Java has no default parameter values. Overloads that delegate to a fuller version provide the same effect:
public class Logger {
public void log(String message) {
log(message, "INFO");
}
public void log(String message, String level) {
log(message, level, System.currentTimeMillis());
}
public void log(String message, String level, long timestamp) {
System.out.printf("%d [%s] %s%n", timestamp, level, message);
}
}
All the real work happens in one method. The shorter versions only supply defaults, so behavior stays consistent and changes are made in one place.
Accepting different input types
public class Temperature {
public static double toFahrenheit(double celsius) {
return celsius * 9 / 5 + 32;
}
public static double toFahrenheit(String celsiusText) {
return toFahrenheit(Double.parseDouble(celsiusText.strip()));
}
public static double[] toFahrenheit(double... celsiusValues) {
double[] out = new double[celsiusValues.length];
for (int i = 0; i < out.length; i++) {
out[i] = toFahrenheit(celsiusValues[i]);
}
return out;
}
}
Temperature.toFahrenheit(100); // 212.0 (double version, int widened)
Temperature.toFahrenheit(" 37 "); // about 98.6 (String version)
Temperature.toFahrenheit(0, 25, 40); // [32.0, 77.0, 104.0] (varargs version)
The single-value call toFahrenheit(100) selects the double version in phase 1, before varargs is ever considered, so the two overloads do not conflict.
Overloads in the JDK
| Method | Overloads for |
|---|---|
System.out.println | every primitive type, char[], String, Object |
Math.max, Math.abs | int, long, float, double |
String.valueOf | every primitive, char[], Object |
Arrays.sort | every primitive array, Object[], ranges, comparators |
List.of | 0 to 10 elements, plus varargs |
List.of has fixed-arity overloads for up to ten elements, plus a varargs version, because calling a fixed-arity method avoids creating a varargs array on every call.
Overloading Across Inheritance
A subclass can add overloads of an inherited method:
class Printer {
void print(String s) { System.out.println("text: " + s); }
}
class ColorPrinter extends Printer {
void print(String s, String color) { // overload, not override
System.out.println(color + " text: " + s);
}
}
ColorPrinter p = new ColorPrinter();
p.print("hi"); // text: hi (inherited)
p.print("hi", "red"); // red text: hi
Be careful not to confuse an accidental overload with an intended override:
class Point {
final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
public boolean equals(Point other) { // OVERLOAD of equals(Object), not an override!
return x == other.x && y == other.y;
}
}
Object a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(b.equals(new Point(1, 2))); // true: calls equals(Point)
System.out.println(a.equals(b)); // false: calls Object.equals(Object)
System.out.println(List.of(b).contains(new Point(1, 2))); // false: collections call equals(Object)
equals(Point) does not override Object.equals(Object), because the parameter type differs. Collections call equals(Object), so the custom version is ignored. Writing @Override on the method would have produced a compile error immediately, revealing the mistake. This is a strong reason to use @Override on every method meant to override.
Guidelines
- Overload only for the same operation. If two methods do different things, give them different names.
- Avoid overloads with the same number of parameters of closely related types.
remove(int index)andremove(Object element)onListare a well-known source of bugs:list.remove(1)on aList<Integer>removes the element at index 1, not the value 1. - Delegate rather than duplicate. Shorter overloads should call the complete one.
- Be cautious mixing overloads with varargs and boxing. The resolution rules are precise but rarely intuitive; prefer distinct names when results could surprise a reader.
List<Integer> nums = new ArrayList<>(List.of(10, 20, 30));
nums.remove(1); // removes index 1 → [10, 30]
nums.remove(Integer.valueOf(10)); // removes the value 10 → [30]
Practical Example: Report Formatter
import java.time.LocalDate;
import java.util.List;
public class Format {
static String cell(String text, int width) {
if (text.length() > width) {
return text.substring(0, width - 1) + "…";
}
return String.format("%-" + width + "s", text);
}
static String cell(double amount, int width) {
return String.format("%," + width + ".2f", amount);
}
static String cell(int count, int width) {
return String.format("%" + width + "d", count);
}
static String cell(LocalDate date, int width) {
return cell(date.toString(), width);
}
static String row(Object... values) {
StringBuilder sb = new StringBuilder();
for (Object v : values) {
sb.append(v).append(" | ");
}
return sb.toString();
}
public static void main(String[] args) {
record Sale(String product, int qty, double amount, LocalDate date) {}
List<Sale> sales = List.of(
new Sale("Wireless Keyboard", 3, 7497, LocalDate.of(2026, 11, 2)),
new Sale("USB-C Docking Station Pro", 1, 12999.5, LocalDate.of(2026, 11, 5))
);
for (Sale s : sales) {
System.out.println(row(
cell(s.product(), 18),
cell(s.qty(), 4),
cell(s.amount(), 12),
cell(s.date(), 10)
));
}
}
}
Output:
Wireless Keyboard | 3 | 7,497.00 | 2026-11-02 |
USB-C Docking Sta… | 1 | 12,999.50 | 2026-11-05 |
One name, cell, formats any kind of value to a fixed width, and the compiler picks the version from the argument's type: text is left-aligned and truncated, whole numbers are right-aligned, money gets two decimals and thousands separators, and dates delegate to the String version. s.qty() is an int, so it selects cell(int, int) exactly rather than widening to double. In the format strings, flags such as - and , must come before the width (%,12.2f, not %12,.2f), which is why the width is inserted after them. Each overload performs the same conceptual job, which is exactly the situation overloading is designed for.