Wrapper Classes ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-10-16

Method Overloading with Widening, Autoboxing and Varargs

A class declares go(long x) and go(Integer x), and you call go(5). Which method runs? Many developers say go(Integer), because the number «just gets boxed». The correct answer is go(long): widening a primitive beats boxing. Below is the full order the compiler follows to make that choice, with 12 examples and their output.

When the argument types of a call do not exactly match any overloaded method, the compiler tries three kinds of conversion, in a fixed order: primitive widening, autoboxing and unboxing, and variable-length arguments (varargs). How overloading works on its own (signatures, rules, constructors) is covered in Method and Constructor Overloading in Java, and varargs syntax in Java Varargs. This lesson focuses on how the three conversions interact and which overloaded method is called in each case.

The Three Phases of Overload Resolution

The compiler picks an overloaded method in three phases defined by the language specification (JLS 15.12.2). It moves on to the next phase only if the current one finds no applicable method at all. If a phase finds several applicable methods, the most specific one is chosen.

Phase Allowed Not allowed Example
1. Strict invocation exact match, primitive widening (byte → int), reference widening (Integer → Object) boxing, unboxing, varargs expansion (a varargs method is seen only as a method with an array parameter) go(5) → go(long)
2. Loose invocation everything from phase 1 plus boxing and unboxing (boxing may be followed by reference widening, unboxing by primitive widening) varargs expansion go(5) → go(Integer) or go(Object)
3. Variable-arity invocation everything from phase 2 plus varargs expansion: separate arguments are collected into an array — go(1, 2) → go(int... x)

The key consequence: the less «magic» a call needs, the earlier a method gets considered. Widening comes before boxing, and boxing comes before varargs. The sections below go through each combination with an example.

Primitive Widening: the Smallest Type Wins

When several methods with parameters of different widths are applicable, the compiler picks the narrowest one that fits: byte and short are widened to int, not to long or double.

public class EasyOver {
    static void go(int x) {
        System.out.print("int ");
    }

    static void go(long x) {
        System.out.print("long ");
    }

    static void go(double x) {
        System.out.print("double ");
    }

    public static void main(String[] args) {
        byte b = 5;
        short s = 5;
        long l = 5;
        float f = 5.0f;
        go(b); // byte -> int
        go(s); // short -> int
        go(l); // long -> long
        go(f); // float -> double
    }
}

Output:

int int long double 

For a float, neither go(int) nor go(long) applies: a float cannot be implicitly narrowed, so go(double) is the only candidate.

Widening vs Boxing: Widening Wins

Primitive widening belongs to phase 1 and boxing to phase 2, so when the choice is between them, widening always wins. Widening is the older conversion: code written before Java 5 must not change its behavior just because autoboxing was added to the language.

public class AddBoxing {
    public static void go(Integer x) {
        System.out.println("Integer");
    }

    public static void go(long x) {
        System.out.println("long");
    }

    public static void main(String[] args) {
        int i = 5;
        go(i); // which go() is called?
    }
}

go(long) is called: in phase 1 the int is widened to long, and boxing is never considered.

long

For an Integer argument the picture is reversed. Compare two calls against methods taking long and Object:

public class WidenOrObject {
    static void go(long x) {
        System.out.println("long");
    }

    static void go(Object x) {
        System.out.println("Object");
    }

    public static void main(String[] args) {
        int i = 5;
        Integer boxed = 5;
        go(i);     // phase 1: int -> long
        go(boxed); // phase 1: Integer -> Object (reference widening)
    }
}
long
Object

The second line surprises many people: for an Integer, go(long) requires unboxing, which is phase 2, while go(Object) is an ordinary reference widening that already applies in phase 1. So go(Object) wins.

Box Then Widen: Allowed; Widen Then Box: Not

A primitive value can be boxed first and then widened as a reference: int → Integer → Object (or Number, Comparable). Such a call is resolved in phase 2.

public class BoxAndWiden {
    public static void go(Object o) {
        Byte b2 = (Byte) o;
        System.out.println(b2);
    }

    public static void main(String[] args) {
        byte b = 5;
        go(b); // byte -> Byte -> Object
    }
}

The byte value is boxed into a Byte and then passed as an Object; the (Byte) o cast inside the method succeeds.

5

What you cannot do is widen a primitive first and then box it: a byte will not become a Long by way of an intermediate long. Nor can one wrapper be widened into another: an Integer is not a Long, because wrapper classes are not related to each other by inheritance.

public class WidenAndBox {
    static void go(Long x) {
        System.out.println("Long");
    }

    public static void main(String[] args) {
        byte b = 5;
        // go(b); // compilation error: byte -> long -> Long is not allowed
        go(5L);   // OK: a long is boxed into Long
    }
}

The go(b) line does not compile: incompatible types: byte cannot be converted to Long. Without it, the program prints Long.

Same rule in assignments

The rule applies without any overloading, too: Long l = 4; does not compile, while Long l = 4L; does. Why Short s = 4; still compiles is explained in Autoboxing and Unboxing in Java: in an assignment a constant may be narrowed, but in a method call it may not (see below).

Unboxing Followed by Widening

A wrapper can be unboxed first and the resulting primitive then widened: the chain Short → short → int is allowed, and such a call is resolved in phase 2. The opposite order does not work: a Short cannot be turned into an Integer to be unboxed into an int, because wrappers are not related by inheritance.

public class UnboxAndWiden {
    static int countGold(int x, int y) {
        return x + y;
    }

    public static void main(String[] args) {
        Short myGold = 7;
        System.out.println(countGold(myGold, 6)); // Short -> short -> int
    }
}
13

Rule out static first

If you remove static from countGold in this example, the program fails to compile for a completely different reason: a non-static method cannot be called from the static main without an object (non-static method countGold(int,int) cannot be referenced from a static context). In «which method is called» exam questions, look for errors like this before you start working through the phases.

Widening vs Varargs: Varargs Loses

A varargs method with separate arguments is considered only in phase 3. If a regular method applies through widening, it wins, even when the varargs parameter type matches the arguments more precisely.

public class AddVarargs {
    public static void go(int x, int y) {
        System.out.println("int,int");
    }

    public static void go(byte... x) {
        System.out.println("byte...");
    }

    public static void main(String[] args) {
        byte b = 5;
        go(b, b); // which go() is called?
    }
}

go(int, int) is called: both byte values are widened to int in phase 1.

int,int

Boxing vs Varargs: Boxing Wins

A method that applies through boxing is found in phase 2, while a varargs method with separate arguments is considered only in phase 3. So when the choice is between boxing and varargs, boxing always wins.

public class BoxOrVararg {
    public static void go(Byte x, Byte y) {
        System.out.println("Byte, Byte");
    }

    public static void go(byte... x) {
        System.out.println("byte...");
    }

    public static void main(String[] args) {
        byte b = 5;
        go(b, b); // which go() is called?
    }
}

go(Byte, Byte) is called: both byte values are boxed in phase 2, and varargs is not needed.

Byte, Byte

Varargs and a Literal: No Narrowing in a Call

Unlike an assignment, a method call never narrows a constant to short or byte. That is why a call with the literal 7 does not compile, even though both invade(short) and invade(short...) exist:

class Alien {
    String invade(short ships) { return "a few"; }
    String invade(short... ships) { return "many"; }
}

public class Defender {
    public static void main(String[] args) {
        // System.out.println(new Alien().invade(7)); // compilation error: an int cannot be passed as a short
        System.out.println(new Alien().invade((short) 7)); // a few
    }
}

The literal 7 is an int; neither short nor short... accepts it, and the compiler reports no suitable method found for invade(int). With an explicit (short) 7 cast, invade(short) applies in phase 1 and the program prints a few.

An Array as the Argument: Varargs in Phases 1 and 2

In phases 1 and 2 a varargs method takes part as an ordinary method with an array parameter: Object... looks like Object[] there. So passing an existing array can select a varargs method without ever reaching phase 3.

public class ArrayArgs {
    static void h(Object o) {
        System.out.println("Object");
    }

    static void h(Object... o) {
        System.out.println("Object...");
    }

    public static void main(String[] args) {
        h(5);               // phase 2: boxing, int -> Integer -> Object
        h(new int[0]);      // int[] is not Object[], only h(Object) applies
        h(new String[0]);   // String[] fits both, Object[] is more specific than Object
        h(1, 2);            // phase 3: the only applicable method
    }
}
Object
Object
Object...
Object...

For h(5), h(Object...) does not apply in phase 2 (the number 5 is not an array), so h(Object) is chosen. For h(new String[0]), both methods apply in phase 1, and the more specific h(Object...) wins, because Object[] is a subtype of Object.

How phase 3 chooses between several varargs methods (int... vs double... and so on) is covered in Overloading with varargs in the varargs lesson.

Ambiguous Method Call: reference is ambiguous

If several methods apply in the same phase and none of them is more specific than the others, the compiler does not pick one at random: it stops with the error reference to go is ambiguous. This can happen with widening as well as with boxing.

public class Ambiguous {
    static void go(long a, int b) {
        System.out.println("long,int");
    }

    static void go(int a, long b) {
        System.out.println("int,long");
    }

    static void box(Integer a, int b) {
        System.out.println("Integer,int");
    }

    static void box(int a, Integer b) {
        System.out.println("int,Integer");
    }

    public static void main(String[] args) {
        // go(1, 1);  // error: reference to go is ambiguous
        // box(1, 1); // error: reference to box is ambiguous
        go(1, 1L);                  // int,long
        box(Integer.valueOf(1), 1); // Integer,int
    }
}

For go(1, 1), both methods apply in phase 1: an int is widened to long in either the first or the second argument. Neither parameter list is better than the other, and javac reports:

error: reference to go is ambiguous
  both method go(long,int) in Ambiguous and method go(int,long) in Ambiguous match

For box(1, 1), no method applies in phase 1; in phase 2 both do (boxing the first or the second argument), and the result is the same error. The fix: cast or convert the arguments so that one method matches exactly. Output of the lines that compile:

int,long
Integer,int

list.remove(int) or remove(Object): What Gets Removed

For a List<Integer>, the call list.remove(1) removes the element at index 1, not the value 1: remove(int index) applies in phase 1 without boxing. To remove a value, pass an object: list.remove(Integer.valueOf(1)) calls remove(Object o).

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

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

        list.remove(1);                   // remove(int index)
        System.out.println(list);

        list.remove(Integer.valueOf(10)); // remove(Object o)
        System.out.println(list);
    }
}
[10, 30]
[30]

The call list.remove(1) is resolved in phase 1: the int argument matches remove(int) exactly, no boxing is needed, and the element at index 1 (the value 20) is removed. For remove(Integer.valueOf(10)), remove(Object) applies in phase 1 as a reference widening, while remove(int) would need unboxing (phase 2), so the value 10 is removed. A practical note on this in the context of collections is in Wrappers in Collections and Generics.

Overload Resolution Rules: Summary

The compiler prefers the «cheapest» conversion: an exact match or widening (phase 1), then boxing and unboxing (phase 2), and only then varargs (phase 3). The chain «boxing + reference widening» is allowed, «primitive widening + boxing» is not. All the rules of this lesson in one table:

Rule Example Result
Among several widenings, the narrowest one is chosen byte with go(int), go(long) go(int)
Widening beats boxing int with go(long), go(Integer) go(long)
You can box and then widen the reference int → Integer → Object the call compiles (phase 2)
You cannot widen a primitive and then box it byte → Long compilation error
You cannot widen one wrapper into another Integer → Long compilation error
You can unbox and then widen the primitive Short → short → int the call compiles (phase 2)
Widening and boxing beat varargs go(b, b) with go(int, int) or go(Byte, Byte) and go(byte...) the regular method
A literal is not narrowed in a method call invade(7) with invade(short) compilation error
Several equally good candidates in the same phase go(1, 1) with go(long, int) and go(int, long) ambiguous method call

Practice: What Does This Program Print?

A typical exam and interview question combines all of these factors at once. Work out the output before you read the explanation.

public class Mixed {
    static String s = "";

    static void f(Object o)     { s += "1"; }
    static void f(Object... o)  { s += "2"; }
    static void f(Integer... i) { s += "3"; }
    static void f(Long l)       { s += "4"; }

    public static void main(String[] args) {
        int x = 4;
        Boolean y = true;
        f(x);
        f(x, x);
        f(y, x);
        f();
        System.out.println(s);
    }
}
1323
  • f(x): no method applies in phase 1 (boxing is required). In phase 2, f(Object) applies (int → Integer → Object), while f(Long) does not, because you cannot widen and then box. Appends 1.
  • f(x, x): no regular method has two parameters, so this is phase 3. Both f(Object...) and f(Integer...) apply; Integer... is more specific. Appends 3.
  • f(y, x): a Boolean cannot be an Integer, so only f(Object...) remains. Appends 2.
  • f(): with no arguments both varargs methods apply, and the more specific f(Integer...) is chosen. Appends 3.

Overloading is resolved by the compiler, so by the time the program runs the choice has already been made. How to see the selected signature of each call in the bytecode is shown in the FAQ below.

Frequently Asked Questions

How do I pass an int to a method that takes a Long?

Make the argument a long before the call: use the literal go(5L), a cast go((long) x) or explicit boxing go(Long.valueOf(x)). The compiler will not build the int → long → Long chain for you. The reason is explained in Box Then Widen: Allowed; Widen Then Box: Not.

What does "reference to method is ambiguous" mean and how do I fix it?

Several overloaded methods applied in the same phase, and none is more specific than the others. Cast the argument to the type you need (1L, (long) a, Integer.valueOf(1)) or rename the methods if they actually do different things. An example with the full javac message is in Ambiguous Method Call.

Which method is called for go(null)?

null fits any reference parameter already in phase 1, so the most specific type is chosen. With go(Object) and go(String), go(String) is called. With go(String) and go(Integer) you get reference to go is ambiguous: the types are not related by inheritance. With go(int) and go(Integer), go(Integer) is called, because a primitive cannot be null.

How can I see which overloaded method the compiler chose?

Run javap -c ClassName: every invokestatic or invokevirtual instruction contains the full signature of the selected method. In an IDE, Ctrl+Click (go to declaration) on the call shows the same thing.

Practice

Reinforce this lesson with hands-on tasks, each with a level and a time estimate.

All module tasks →

Comments

Please log in or register to have a possibility to add comment.