В перегруженном API обычный метод применим напрямую, а другой кандидат требует преобразования varargs: какой кандидат выберет Java и почему?
Java выберет обычный метод, если он применим без переменноарного преобразования. Разрешение перегрузки выполняется поэтапно: сначала проверяются обычные вызовы фиксированной арности, и только затем — вызовы с упаковкой аргументов в массив varargs.
Varargs появились в Java как удобный способ передавать переменное число аргументов без ручного создания массива. При этом требовалось сохранить предсказуемость уже существующих перегрузок: добавление varargs-варианта не должно было без необходимости перехватывать вызовы, подходящие обычному методу.
Поэтому компилятор применяет строгий порядок фаз разрешения перегрузки. Более простая форма вызова получает приоритет над преобразованием к переменной арности.
Рассмотрим API, в котором есть обычная перегрузка и перегрузка с varargs. Если компилятор выбирал бы только самый «подходящий на вид» метод, поведение вызова могло бы зависеть от деталей упаковки аргументов в массив.
Неверное понимание механизма приводит к ошибочному выводу, что varargs всегда конкурирует с обычным методом на равных условиях. На практике varargs-адаптация является более поздней фазой и используется только при отсутствии подходящего кандидата на предыдущих фазах.
Разрешение перегрузки происходит во время компиляции и основывается прежде всего на статических типах аргументов. Упрощённо компилятор проходит такие этапы:
Если обычная перегрузка найдена на более ранней фазе, до varargs-кандидата компилятор не доходит. Поэтому метод с фиксированной арностью обычно выигрывает у метода, которому нужно собрать переданные значения в массив.
Вызов write("event") выберет write(Object): строка может быть передана как Object без varargs-преобразования. Метод write(Object...) потребовал бы сформировать массив Object[], поэтому он рассматривается позже.
Важно отличать это от случая, когда аргумент уже является массивом подходящего типа. Тогда varargs-метод может быть вызван в режиме фиксированной арности, без упаковки отдельных аргументов в новый массив, и правило о его проигрыше как varargs-кандидата напрямую не применяется.
В библиотеке логирования существовали методы для одного значения и для произвольного числа значений. После добавления varargs-разновидности команда обнаружила, что разработчики неверно ожидали вызов многозначной версии даже для одного аргумента.
Вариант с одним универсальным varargs-методом уменьшил число перегрузок, но сделал намерение вызова менее очевидным и мог создавать массивы при каждом обращении. Вариант с отдельной фиксированной перегрузкой требовал больше API-кода, зато сохранял предсказуемое разрешение и позволял оптимизировать частый однозначный вызов.
Был выбран второй вариант: для одного значения используется фиксированная перегрузка, а varargs — только для действительно переменного числа аргументов. Это уменьшило лишние упаковки и сделало контракт API понятнее.
1. Всегда ли varargs-метод проигрывает фиксированной перегрузке?
Нет. Varargs-метод может рассматриваться как обычный метод фиксированной арности, если переданный аргумент совместим непосредственно с его параметром-массивом. Например, передача уже созданного массива может не требовать упаковки отдельных элементов. В таком случае решающей становится обычная проверка перегрузок, а не последняя varargs-фаза.
2. Что произойдёт, если фиксированная перегрузка требует boxing, а varargs-кандидат — переменной арности?
Фиксированная перегрузка всё равно будет предпочтительнее, потому что фаза с boxing и unboxing выполняется раньше фазы переменной арности. Varargs не получает приоритет только потому, что его параметр визуально кажется более общим или удобным для вызова.
3. Может ли добавление новой перегрузки изменить поведение уже существующего исходного кода?
Да. Если новая фиксированная перегрузка становится применимой на более ранней фазе, она может начать выбираться вместо прежней varargs-перегрузки. Поэтому добавление перегрузок сохраняет компилируемость не всегда означает сохранение поведения: необходимо проверять разрешение вызовов и возможные изменения выбранной реализации.