Перегрузка методов с автоупаковкой, расширением и 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.
Практика по теме
Закрепите урок на задачах — у каждой есть уровень и оценка времени.
Все задачи модуля →Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии