Skip to main content
Java · lesson 21 of 66
Open the Java page →
Java intermediate Lesson 21 of 66

Method Overloading in Java

Learn Java method overloading by running it: how the compiler picks an overload through widening, boxing and varargs, plus the null and remove(int) traps.

By SkillByExample · Updated

Overloading means several methods share a name but differ in their parameter lists. The compiler picks one at compile time from the argument types, so it is sometimes called compile-time polymorphism. Every example is a single file run with the JDK 21 source launcher (java File.java).

Convenience overloads that delegate

The usual reason to overload is a short form for the common case that forwards to the full version.

public class Greeter {
    static String greet(String name) {
        return greet(name, "Hello", 1);
    }

    static String greet(String name, String greeting) {
        return greet(name, greeting, 1);
    }

    static String greet(String name, String greeting, int times) {
        return (greeting + ", " + name + "! ").repeat(times).trim();
    }

    public static void main(String[] args) {
        System.out.println(greet("Ada"));
        System.out.println(greet("Ada", "Hi"));
        System.out.println(greet("Ada", "Yo", 3));
    }
}
$ java Greeter.java
Hello, Ada!
Hi, Ada!
Yo, Ada! Yo, Ada! Yo, Ada!

Only the three-argument version contains logic; the others fill in defaults. Java has no default parameter values, so this pattern stands in for them.

Return type alone is not enough

The compiler distinguishes overloads by parameter types only.

public class ReturnOnly {
    static int parse(String s) { return Integer.parseInt(s); }

    static long parse(String s) { return Long.parseLong(s); }

    public static void main(String[] args) {
        System.out.println(parse("42"));
    }
}
$ java ReturnOnly.java
ReturnOnly.java:4: error: method parse(String) is already defined in class ReturnOnly
    static long parse(String s) { return Long.parseLong(s); }
                ^
1 error
error: compilation failed

At the call parse("42") nothing says which return type is wanted, so Java rejects the second declaration outright rather than the call.

How the compiler picks: widening, boxing, varargs

When no exact match exists, the compiler tries widening first, then boxing, then varargs.

public class Resolution {
    static void take(long x)     { System.out.println("long (widening)"); }
    static void take(Integer x)  { System.out.println("Integer (boxing)"); }
    static void take(int... xs)  { System.out.println("int... (varargs)"); }
    static void take(Object x)   { System.out.println("Object"); }

    public static void main(String[] args) {
        int i = 5;
        take(i);                 // int -> long beats int -> Integer
        take(Integer.valueOf(5));
        take("text");
        take();                  // only varargs accepts zero arguments
        take(1, 2);
    }
}
$ java Resolution.java
long (widening)
Integer (boxing)
Object
int... (varargs)
int... (varargs)

Phase one (no boxing, no varargs) finds take(long) for an int, so the boxed version never gets a chance. Boxing rules were added in Java 5 and placed after widening so that old code kept calling the same methods.

The remove(int) trap

List has remove(int index) and remove(Object o) — and an int argument always picks the first.

import java.util.ArrayList;
import java.util.List;

public class RemoveTrap {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(10, 20, 1, 30));

        nums.remove(1);                     // index 1, i.e. 20
        System.out.println(nums);

        nums.remove(Integer.valueOf(1));    // the value 1
        System.out.println(nums);
    }
}
$ java RemoveTrap.java
[10, 1, 30]
[10, 30]

This is the widening-before-boxing rule again: remove(int) is applicable without boxing, so remove(Object) is never considered. Box explicitly when you mean the value.

null and ambiguous calls

null fits any reference type. If two overloads are equally specific, the call does not compile.

public class Ambiguous {
    static void show(String s)  { System.out.println("String"); }
    static void show(Integer n) { System.out.println("Integer"); }
    static void show(Object o)  { System.out.println("Object"); }

    public static void main(String[] args) {
        show((String) null);
        show(null);
    }
}
$ java Ambiguous.java
Ambiguous.java:8: error: reference to show is ambiguous
        show(null);
        ^
  both method show(String) in Ambiguous and method show(Integer) in Ambiguous match
1 error
error: compilation failed

String and Integer are both more specific than Object, but neither is more specific than the other, so show(null) is ambiguous. A cast picks one; with only show(String) and show(Object), the String version would win without one.

Overloads are chosen from the declared type

Because overloading is resolved at compile time, the declared type of the argument decides, not the object’s runtime class.

public class StaticChoice {
    static class Animal {}
    static class Dog extends Animal {}

    static String describe(Animal a) { return "an animal"; }
    static String describe(Dog d)    { return "a dog"; }

    public static void main(String[] args) {
        Dog dog = new Dog();
        Animal alsoDog = new Dog();
        System.out.println(describe(dog));
        System.out.println(describe(alsoDog));
        System.out.println(alsoDog.getClass().getSimpleName());
    }
}
$ java StaticChoice.java
a dog
an animal
Dog

alsoDog really is a Dog, but its declared type is Animal, so the compiler bound the call to describe(Animal). Overriding is the mechanism that dispatches on the runtime class — see the polymorphism lesson.

Overloaded constructors with this(…)

Constructors overload the same way and chain to each other with this(...).

public class Rect {
    final int width, height;

    Rect(int width, int height) {
        this.width = width;
        this.height = height;
    }

    Rect(int side) {
        this(side, side);               // must be the first statement
    }

    Rect() {
        this(1);
    }

    public static void main(String[] args) {
        for (Rect r : new Rect[] {new Rect(3, 4), new Rect(5), new Rect()}) {
            System.out.println(r.width + "x" + r.height + " area " + r.width * r.height);
        }
    }
}
$ java Rect.java
3x4 area 12
5x5 area 25
1x1 area 1

Each shorter constructor delegates, so field assignment happens in exactly one place.

Practice

  1. Write max overloads for two ints, three ints, and two doubles, where the three-argument version reuses the two-argument one. Call each.
Solution
public class Max {
    static int max(int a, int b) { return a >= b ? a : b; }

    static int max(int a, int b, int c) { return max(max(a, b), c); }

    static double max(double a, double b) { return a >= b ? a : b; }

    public static void main(String[] args) {
        System.out.println(max(3, 7));
        System.out.println(max(3, 9, 7));
        System.out.println(max(2.5, 1.5));
        System.out.println(max(2, 1.5));   // int widens to double
    }
}
$ java Max.java
7
9
2.5
2.0

max(2, 1.5) has no (int, double) overload, so the int widens and max(double, double) runs.

  1. Predict, then check: given f(long) and f(Integer), which runs for f(5)? Add f(int...) and call f() too.
Solution
public class Predict {
    static void f(long x)    { System.out.println("long"); }
    static void f(Integer x) { System.out.println("Integer"); }
    static void f(int... x)  { System.out.println("varargs " + x.length); }

    public static void main(String[] args) {
        f(5);
        f();
    }
}
$ java Predict.java
long
varargs 0

Widening (int to long) is phase one; boxing to Integer is phase two, so it never runs here.

  1. Fix Ambiguous so show(null) compiles without a cast, by changing only the overloads.
Solution
public class NotAmbiguous {
    static void show(String s)  { System.out.println("String"); }
    static void show(Object o)  { System.out.println("Object"); }

    public static void main(String[] args) {
        show(null);
        show(42);
    }
}
$ java NotAmbiguous.java
String
Object

With Integer gone, String is strictly more specific than Object, so it wins for null; 42 boxes to Integer and falls through to Object.

  1. Write sum(int... nums) plus a dedicated sum(int a, int b). Show which runs for two arguments and which for three.
Solution
public class Sums {
    static int sum(int a, int b) {
        System.out.print("fixed: ");
        return a + b;
    }

    static int sum(int... nums) {
        System.out.print("varargs: ");
        int total = 0;
        for (int n : nums) total += n;
        return total;
    }

    public static void main(String[] args) {
        System.out.println(sum(1, 2));
        System.out.println(sum(1, 2, 3));
    }
}
$ java Sums.java
fixed: 3
varargs: 6

Varargs is the last phase, so a fixed-arity overload always wins when it applies — a common way to avoid allocating an array on hot paths.

Frequently Asked Questions

Can you overload methods by return type only?
No. Two methods with the same name and parameter types but different return types are a compile error ('method is already defined'). The compiler chooses an overload from the arguments alone, so the return type cannot tell the two apart.
In what order does Java resolve overloaded methods?
The compiler tries three phases in order: first an exact match or primitive widening with no boxing, then matches that need autoboxing or unboxing, and finally varargs. The first phase that finds an applicable method wins, and within a phase the most specific method is chosen.
What is the difference between overloading and overriding?
Overloading is several methods with the same name but different parameter lists, chosen at compile time from the declared types of the arguments. Overriding is a subclass replacing an inherited method with the same signature, chosen at runtime from the object's actual class.
Why does list.remove(1) remove the element at index 1 instead of the value 1?
List has two overloads, remove(int index) and remove(Object o). An int argument matches remove(int) exactly in the first phase, so boxing to Integer is never considered. Write list.remove(Integer.valueOf(1)) to remove by value.