Varargs в Java: методы с переменным числом аргументов - Вопросы

Всего: 5 вопросов

1. 

Что такое 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-параметра при объявлении метода?

Правил три:

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(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 и когда возникает ошибка неоднозначности?

Компилятор подбирает метод в три фазы и переходит к следующей, только если на текущей нет ни одного подходящего кандидата:

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)?

Если 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. На обычном методе экземпляра это ошибка компиляции, потому что наследник мог бы нарушить обещанную безопасность.

Страница 1 из 1