Varargs в Java: методы с переменным числом аргументов - Вопросы
Всего: 5 вопросов
1. Что такое varargs в Java и как объявить метод с переменным числом аргументов?
Что такое varargs в Java и как объявить метод с переменным числом аргументов?
Varargs (от англ. variable arguments) — это синтаксис Java, позволяющий объявить метод, принимающий переменное число аргументов. Возможность появилась в Java 5. Параметр переменной длины объявляется тремя точками после типа: static void test(int... array).
Такой метод можно вызвать с любым количеством аргументов, включая ноль: test(), test(1), test(1, 2, 3), а также передать готовый массив — test(new int[]{1, 2, 3}).
В стандартной библиотеке varargs используется повсеместно: System.out.printf(String, Object...), String.format(String, Object...), List.of(E...), Arrays.asList(T...).
2. Какие правила действуют для varargs-параметра при объявлении метода?
Какие правила действуют для varargs-параметра при объявлении метода?
Правил три:
1. Varargs-параметр должен быть последним в списке параметров: static void test(double d, int... array) — корректно, а static void test(int... array, double d) не компилируется. Иначе компилятор не смог бы понять, где заканчивается список переменной длины.
2. Varargs-параметр в методе может быть только один: static void test(int... a, double... b) — ошибка компиляции.
3. Внутри метода параметр является обычным массивом: доступны length, индексация, цикл for-each, Arrays.toString().
Правила одинаковы для методов, конструкторов, абстрактных и обобщённых методов. Точку входа тоже можно объявить через varargs: public static void main(String... args) полностью равнозначно String[] args.
3. Что приходит в varargs-параметр, если вызвать метод без аргументов, и почему вызов test(null) опасен?
Что приходит в varargs-параметр, если вызвать метод без аргументов, и почему вызов test(null) опасен?
Varargs — это синтаксический сахар над массивами: компилятор сам создаёт массив в точке вызова, поэтому test(1, 2, 3) и test(new int[]{1, 2, 3}) после компиляции неразличимы.
При вызове без аргументов приходит пустой массив, а не null, поэтому array.length вернёт 0, а for-each просто не сделает ни одной итерации. Проверка if (array == null) при обычных вызовах бессмысленна.
Однако вызов test(null) для метода test(String... s) передаёт null вместо всего массива, а не «один элемент null», — и следующий же s.length бросит NullPointerException. Чтобы передать один элемент null, нужно писать test((String) null).
4. Как компилятор выбирает метод при перегрузке с varargs и когда возникает ошибка неоднозначности?
Как компилятор выбирает метод при перегрузке с varargs и когда возникает ошибка неоднозначности?
Компилятор подбирает метод в три фазы и переходит к следующей, только если на текущей нет ни одного подходящего кандидата:
1. Строгое соответствие — точное совпадение и расширение примитивов (без автоупаковки и varargs): test(3) выберет test(int a).
2. С автоупаковкой — учитывается boxing/unboxing, varargs по-прежнему игнорируется: test(3) выберет test(Integer a).
3. С varargs — только теперь рассматриваются методы с переменным числом аргументов: test(1, 2) выберет test(int... a).
Отсюда главное правило: varargs-метод рассматривается последним. Если на третьей фазе подходят сразу несколько varargs-методов, выбирается наиболее специфичный — например, test(int... array) специфичнее test(double... array), потому что int расширяется до double.
Если ни один кандидат не является более специфичным (например, test(boolean... a) и test(int... a) при вызове test()), компилятор выдаёт ошибку reference to test is ambiguous — программа не собирается, до запуска JVM дело не доходит. Обойти это можно явным пустым массивом test(new int[0]) или разными именами методов.
5. Зачем нужна аннотация @SafeVarargs и что такое загрязнение кучи (heap pollution)?
Зачем нужна аннотация @SafeVarargs и что такое загрязнение кучи (heap pollution)?
Если varargs-параметр имеет обобщённый тип (T... или List<String>...), компилятор предупреждает о возможном загрязнении кучи: из-за стирания типов массив List<String>[] во время выполнения является обычным List[], и в него можно положить что угодно — ошибка всплывёт позже как ClassCastException.
Если метод только читает элементы и ничего не записывает в массив, предупреждение снимают аннотацией @SafeVarargs:
@SafeVarargs
static <T> List<T> toList(T... items) {
return new ArrayList<>(Arrays.asList(items));
}
Ставить её можно только на методы, которые нельзя переопределить: static, final, конструкторы, а с Java 9 — ещё и private. На обычном методе экземпляра это ошибка компиляции, потому что наследник мог бы нарушить обещанную безопасность.