Wrapper классы ·
‹ Предыдущий Следующий ›
⏱ 5 минут чтения Обновлено: 2026-11-04

Перегрузка методов с автоупаковкой, расширением и varargs

В классе объявлены go(long x) и go(Integer x), а вызывается go(5). Какой метод сработает? Многие отвечают go(Integer), потому что число «просто упаковывается». Правильный ответ — go(long): расширение примитива побеждает упаковку. Ниже — полный порядок, по которому компилятор делает такой выбор, и 12 примеров с выводом.

Когда у перегруженных методов совпадают не все типы, компилятор последовательно пробует три способа привести аргументы к параметрам: расширение примитивов, автоупаковку и распаковку, аргументы переменной длины (varargs). Как работает перегрузка сама по себе (сигнатура, правила, конструкторы), рассказано в уроке Перегрузка методов и конструкторов, синтаксис varargs — в уроке Varargs в Java. Здесь разберем только их взаимодействие и покажем, какой метод вызовется в каждом случае.

Три фазы выбора перегруженного метода

Компилятор выбирает перегруженный метод в три фазы (спецификация языка, JLS 15.12.2). Он переходит к следующей фазе, только если на текущей не нашлось ни одного подходящего метода. Если подходящих несколько, из них выбирается наиболее специфичный.

Фаза Что разрешено Что запрещено Пример
1. Строгий вызов точное совпадение, расширение примитивов (byte → int), расширение ссылок (Integer → Object) упаковка, распаковка, раскрытие varargs (varargs-метод виден только как метод с параметром-массивом) go(5) → go(long)
2. Нестрогий вызов фаза 1 плюс упаковка и распаковка (после упаковки допустимо расширение ссылки, после распаковки — расширение примитива) раскрытие varargs go(5) → go(Integer) или go(Object)
3. Вызов с переменным числом аргументов фаза 2 плюс раскрытие varargs: отдельные аргументы собираются в массив — go(1, 2) → go(int... x)

Отсюда главное следствие: чем меньше «магии» нужно для вызова, тем раньше метод попадает в выбор. Расширение раньше упаковки, упаковка раньше varargs. Дальше разберем каждое сочетание на примере.

Расширение примитивов: побеждает наименьший тип

Если подходит несколько методов с параметрами разной ширины, компилятор выбирает наименьший из подходящих: byte и short расширяются до int, а не до long или 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
    }
}

Результат выполнения:

int int long double 

Для float методы go(int) и go(long) не подходят: float нельзя неявно сузить, поэтому остается только go(double).

Расширение или упаковка: побеждает расширение

Расширение примитива относится к фазе 1, упаковка — к фазе 2, поэтому между ними всегда выигрывает расширение. Это исторически более старое преобразование: код, написанный до Java 5, не должен менять поведение из-за появления автоупаковки.

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); // какой go() вызовется?
    }
}

Вызовется go(long): на фазе 1 int расширяется до long, до упаковки дело не доходит.

long

Для Integer картина обратная. Сравните два вызова с параметрами long и 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);     // фаза 1: int -> long
        go(boxed); // фаза 1: Integer -> Object (расширение ссылки)
    }
}
long
Object

Вторая строка удивляет: для Integer метод go(long) требует распаковки, то есть фазы 2, а go(Object) подходит как обычное расширение ссылки уже на фазе 1. Побеждает go(Object).

Упаковка, а потом расширение: можно; наоборот: нельзя

Значение примитива можно сначала упаковать, а потом расширить ссылку: int → Integer → Object (или Number, Comparable). Такой вызов относится к фазе 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
    }
}

Значение byte упаковывается в Byte, а затем передается как Object; приведение (Byte) o внутри метода проходит успешно.

5

А вот сначала расширить примитив, а потом упаковать нельзя: byte не станет Long через промежуточное long. Нельзя и расширить одну обертку до другой: Integer не является Long, классы-обертки не связаны наследованием друг с другом.

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

    public static void main(String[] args) {
        byte b = 5;
        // go(b); // ошибка компиляции: byte -> long -> Long запрещено
        go(5L);   // подходит: long упаковывается в Long
    }
}

Строка go(b) не компилируется: incompatible types: byte cannot be converted to Long. Программа без нее выводит Long.

Связь с присваиванием

То же правило действует и без перегрузки: Long l = 4; не компилируется, а Long l = 4L; — да. Почему при этом проходит Short s = 4;, объяснено в уроке Autoboxing и unboxing: в присваивании константа может быть сужена, а в вызове метода — нет (см. ниже).

Распаковка и расширение

Обертку можно сначала распаковать, а потом расширить полученный примитив: цепочка Short → short → int допустима, и такой вызов относится к фазе 2. Обратный порядок не работает: превратить Short в Integer, чтобы потом распаковать его в int, нельзя, потому что обертки не связаны наследованием.

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

Сначала проверьте static

Если в этом примере убрать static у countGold, программа не скомпилируется по другой причине: нестатический метод нельзя вызвать из статического main без объекта (non-static method countGold(int,int) cannot be referenced from a static context). В задачах на выбор метода сначала ищите такие ошибки, а уже потом разбирайте фазы.

Расширение и varargs: varargs проигрывает

Varargs-метод рассматривается только на фазе 3. Если есть обычный метод, подходящий за счет расширения, он выигрывает, даже если типы varargs совпадают с аргументами точнее.

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); // какой go() вызовется?
    }
}

Вызовется go(int, int): оба byte расширяются до int на фазе 1.

int,int

Упаковка и varargs: упаковка выигрывает

Метод, который подходит за счет упаковки, находится уже на фазе 2, а varargs-метод с отдельными аргументами рассматривается только на фазе 3. Поэтому при выборе между упаковкой и varargs всегда побеждает упаковка.

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); // какой go() вызовется?
    }
}

Вызовется go(Byte, Byte): оба byte упаковываются на фазе 2, varargs не нужен.

Byte, Byte

Varargs и литерал: сужения в вызове нет

В вызове метода, в отличие от присваивания, константа не сужается до short или byte. Поэтому вызов с литералом 7 не скомпилируется, хотя есть и метод invade(short), и метод invade(short...):

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)); // ошибка компиляции: int нельзя передать как short
        System.out.println(new Alien().invade((short) 7)); // a few
    }
}

Литерал 7 имеет тип int, ни short, ни short... его не принимают, и компилятор сообщает об ошибке (no suitable method found for invade(int)). С явным приведением (short) 7 подходит invade(short) на фазе 1, и программа выводит a few.

Массив как аргумент: varargs в фазах 1 и 2

Varargs-метод на фазах 1 и 2 участвует в выборе как обычный метод с параметром-массивом: Object... там выглядит как Object[]. Поэтому передача готового массива может выбрать varargs-метод, не дожидаясь фазы 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);               // фаза 2: boxing, int -> Integer -> Object
        h(new int[0]);      // int[] не Object[], подходит только h(Object)
        h(new String[0]);   // String[] подходит обоим, Object[] специфичнее Object
        h(1, 2);            // фаза 3: единственный подходящий метод
    }
}
Object
Object
Object...
Object...

В h(5) метод h(Object...) на фазе 2 не подходит (число 5 не массив), поэтому выбирается h(Object). Для h(new String[0]) подходят оба метода на фазе 1, и побеждает более специфичный h(Object...), потому что Object[] является подтипом Object.

Какой из нескольких varargs-методов выбирает фаза 3 (int... против double... и т. п.), разобрано в разделе Перегрузка методов с varargs урока про varargs.

Неоднозначный вызов: ambiguous method call

Если на одной фазе подходят несколько методов и ни один не является более специфичным, компилятор не выбирает наугад, а останавливается с ошибкой reference to go is ambiguous. Ошибка возможна как при расширении, так и при упаковке.

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);  // ошибка: reference to go is ambiguous
        // box(1, 1); // ошибка: reference to box is ambiguous
        go(1, 1L);                  // int,long
        box(Integer.valueOf(1), 1); // Integer,int
    }
}

Для go(1, 1) на фазе 1 подходят оба метода: int расширяется до long в первом или втором аргументе. Ни одна пара параметров не лучше другой, и javac сообщает:

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

Для box(1, 1) на фазе 1 подходящих методов нет, на фазе 2 подходят оба (упаковка первого или второго аргумента), и ситуация та же. Исправление: приведите аргументы так, чтобы подошел точный вариант одного из методов. Вывод рабочих строк:

int,long
Integer,int

list.remove(int) или remove(Object): что удалится

Для List<Integer> вызов list.remove(1) удаляет элемент с индексом 1, а не значение 1: метод remove(int index) подходит на фазе 1 без упаковки. Чтобы удалить значение, передайте объект: list.remove(Integer.valueOf(1)) вызовет 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]

Вызов list.remove(1) проходит фазу 1: аргумент int точно подходит к remove(int), упаковка не понадобилась, и удаляется элемент с индексом 1 (значение 20). Для remove(Integer.valueOf(10)) метод remove(Object) подходит на фазе 1 как расширение ссылки, а remove(int) потребовал бы распаковки (фаза 2), поэтому удаляется значение 10. Практическую заметку об этом в контексте коллекций см. в уроке Обертки в коллекциях и дженериках.

Правила выбора перегруженного метода: итог

Компилятор предпочитает самое «дешевое» преобразование: точное совпадение и расширение (фаза 1), затем упаковку и распаковку (фаза 2), и только потом varargs (фаза 3). Цепочка «упаковка + расширение ссылки» разрешена, «расширение примитива + упаковка» — нет. Все правила урока в одной таблице:

Правило Пример Результат
Из нескольких расширений выбирается наименьшее byte при go(int), go(long) go(int)
Расширение побеждает упаковку int при go(long), go(Integer) go(long)
Можно упаковать, а потом расширить ссылку int → Integer → Object вызов допустим (фаза 2)
Нельзя расширить примитив, а потом упаковать byte → Long ошибка компиляции
Нельзя расширить одну обертку до другой Integer → Long ошибка компиляции
Можно распаковать, а потом расширить примитив Short → short → int вызов допустим (фаза 2)
Расширение и упаковка побеждают varargs go(b, b) при go(int, int) или go(Byte, Byte) и go(byte...) обычный метод
Литерал не сужается в вызове метода invade(7) при invade(short) ошибка компиляции
Несколько равноправных кандидатов на одной фазе go(1, 1) при go(long, int) и go(int, long) ambiguous method call

Разбор задачи: что выведет программа

Типичная задача на экзамене и собеседовании — комбинация всех факторов сразу. Определите вывод, прежде чем читать разбор.

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): на фазе 1 нет подходящих методов (нужна упаковка). На фазе 2 подходит f(Object) (int → Integer → Object), а f(Long) не подходит, потому что расширить и упаковать нельзя. Добавляется 1.
  • f(x, x): двух параметров нет ни у одного обычного метода, поэтому фаза 3. Подходят f(Object...) и f(Integer...); Integer... специфичнее, добавляется 3.
  • f(y, x): Boolean не может быть Integer, остается только f(Object...). Добавляется 2.
  • f(): без аргументов подходят оба varargs-метода, выбирается более специфичный f(Integer...). Добавляется 3.

Перегрузку разрешает компилятор, поэтому при запуске программы выбор уже сделан. Как посмотреть выбранную сигнатуру каждого вызова в байт-коде, показано в FAQ ниже.

Часто задаваемые вопросы

Как передать int в метод с параметром Long?

Сделайте аргумент типом long до вызова: литерал go(5L), приведение go((long) x) или явная упаковка go(Long.valueOf(x)). Сам компилятор цепочку int → long → Long не построит. Почему — в разделе Упаковка, а потом расширение.

Что означает ошибка reference to method is ambiguous и как ее исправить?

На одной фазе подошли несколько перегруженных методов, и ни один не специфичнее другого. Приведите аргумент к нужному типу (1L, (long) a, Integer.valueOf(1)) или переименуйте методы, если они делают разные вещи. Пример с текстом ошибки javac — в разделе Неоднозначный вызов.

Какой метод вызовется для go(null)?

null подходит к любому ссылочному параметру уже на фазе 1, поэтому выбирается самый специфичный тип. При go(Object) и go(String) вызовется go(String). При go(String) и go(Integer) — ошибка reference to go is ambiguous: типы не связаны наследованием. При go(int) и go(Integer) вызовется go(Integer), потому что примитив не может быть null.

Как узнать, какой перегруженный метод выбрал компилятор?

Выполните javap -c ИмяКласса: в каждой инструкции invokestatic или invokevirtual записана полная сигнатура выбранного метода. В IDE то же показывает переход к объявлению метода по Ctrl+Click.

Практика по теме

Закрепите урок на задачах — у каждой есть уровень и оценка времени.

Все задачи модуля →

Видео объяснение

Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.

Комментарии

Зарегистрируйтесь или войдите, чтобы иметь возможность оставить комментарий.