Varargs в Java: методы с переменным числом аргументов
Varargs (от англ. variable arguments) — это синтаксис Java, позволяющий объявить метод, который принимает переменное число аргументов. Такие методы ещё называют методами с аргументами переменной длины. Возможность появилась в Java 5 и с тех пор не менялась.
Для объявления параметра переменной длины используются три точки после типа:
static void test(int... array) Вызывать такой метод можно с любым количеством аргументов — включая ноль:
test(); // 0 аргументов
test(1); // 1 аргумент
test(1, 2, 3); // 3 аргумента
test(new int[]{1, 2, 3}); // массив тоже подойдёт Правила объявления varargs
Наряду с параметром переменной длины у метода могут быть и обычные параметры. Но параметр переменной длины должен быть последним в списке, и он может быть только один:
static void test(double d, int... array) // корректно
// static void test(int... array, double d) // ошибка компиляции
// static void test(int... a, double... b) // ошибка компиляции | Правило | Пример | Результат |
|---|---|---|
| Три точки ставятся после типа параметра | void test(int... a) | Метод принимает 0 и более значений int |
| Varargs-параметр должен быть последним | void test(String s, int... a) | Корректно; обратный порядок не компилируется |
| Varargs-параметр в методе может быть только один | void test(int... a, long... b) | Ошибка компиляции |
| Внутри метода параметр — это массив | a.length, a[0] | Доступны все операции с массивом |
| Вызов без аргументов допустим | test() | Передаётся пустой массив, a.length == 0 |
Полезно знать
Точку входа тоже можно объявить через varargs: public static void main(String... args) — это полностью равнозначно записи String[] args, и JVM запустит такую программу без каких-либо оговорок.
Пример: метод с переменным числом аргументов
В примере ниже метод test() объявлен с переменным количеством аргументов типа int. Внутри метода мы обращаемся к параметру как к обычному массиву. При вызове можно передать любое количество значений — в том числе ноль, а также готовый массив:
public class VarArgsExample {
static void test(int... array) {
System.out.println("Количество аргументов: " + array.length);
for (int a : array) {
System.out.print(a + " ");
}
System.out.println();
}
public static void main(String[] args) {
test();
test(1);
test(1, 2);
test(new int[]{1, 3});
}
} Результат выполнения:
Количество аргументов: 0
Количество аргументов: 1
1
Количество аргументов: 2
1 2
Количество аргументов: 2
1 3 Varargs под капотом: это обычный массив
Varargs — это синтаксический сахар над массивами. Компилятор сам создаёт массив в точке вызова и подставляет его в качестве аргумента, поэтому следующие две строки после компиляции неразличимы:
test(1, 2, 3);
// компилятор превращает вызов выше в такой:
test(new int[]{1, 2, 3}); Из этого следуют два практических вывода:
- внутри метода параметру доступны
length, индексация, циклfor-each,Arrays.toString()и всё остальное, что применимо к массиву; - при вызове без аргументов приходит пустой массив, а не
null, поэтомуarray.lengthбезопасно вызывать всегда.
Важно
Каждый вызов varargs-метода создаёт новый массив в куче. В обычном коде это незаметно, но в горячих участках аллокации складываются. Именно поэтому в JDK у методов вроде List.of() есть отдельные перегрузки на 0–10 элементов, а varargs-вариант подключается только начиная с одиннадцатого аргумента.
Перегрузка методов с varargs
Методы с переменным числом аргументов участвуют в перегрузке по особым правилам. Компилятор подбирает метод в три фазы и переходит к следующей, только если на текущей не нашлось ни одного подходящего кандидата.
| Фаза | Что учитывается | Что не учитывается | Пример вызова |
|---|---|---|---|
| 1. Строгое соответствие | Точное совпадение и расширение примитивов | Автоупаковка, varargs | test(3) → test(int a) |
| 2. С автоупаковкой | Autoboxing/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, но не наоборот:
public class VarArgsExample2 {
static void test(double... array) {
System.out.println("test(double... array)");
System.out.println("Количество аргументов: " + array.length);
for (double a : array) {
System.out.print(a + " ");
}
System.out.println();
}
static void test(int... array) {
System.out.println("test(int... array)");
System.out.println("Количество аргументов: " + array.length);
for (int a : array) {
System.out.print(a + " ");
}
System.out.println();
}
static void test(int a) {
System.out.println("test(int a)");
}
public static void main(String[] args) {
test();
test(3);
test(1.0);
test(1, 2);
}
} Результат выполнения:
test(int... array)
Количество аргументов: 0
test(int a)
test(double... array)
Количество аргументов: 1
1.0
test(int... array)
Количество аргументов: 2
1 2 Разберём каждый вызов:
test()— обычные методы не подходят, работает фаза 3; из двух varargs-кандидатов выбран более специфичныйtest(int... array);test(3)— на первой же фазе подходитtest(int a), до varargs дело не доходит;test(1.0)—doubleнельзя сузить доint, поэтому остаётся толькоtest(double... array);test(1, 2)— двух аргументов не принимает ни один обычный метод, снова побеждаетtest(int... array).
Ошибка неоднозначности
При перегрузке методов с аргументами переменной длины легко получить неоднозначность: несколько кандидатов подходят одинаково хорошо, и ни один из них не является более специфичным. Такую ситуацию обнаруживает компилятор на этапе сборки — программа просто не соберётся, до запуска JVM дело не доходит.
В примере ниже метод test перегружен: один вариант принимает varargs типа boolean, второй — varargs типа int. Между boolean и int нет отношения расширения, поэтому при вызове test() без аргументов выбрать метод невозможно:
public class VarArgsExample3 {
static void test(boolean... array) {
System.out.println("test(boolean... array)");
System.out.println("Количество аргументов: " + array.length);
for (boolean a : array) {
System.out.print(a + " ");
}
System.out.println();
}
static void test(int... array) {
System.out.println("test(int... array)");
System.out.println("Количество аргументов: " + array.length);
for (int a : array) {
System.out.print(a + " ");
}
System.out.println();
}
public static void main(String[] args) {
// test(); // ошибка компиляции: reference to test is ambiguous
test(3);
test(1, 2);
}
} Если раскомментировать вызов test(), компилятор выдаст:
error: reference to test is ambiguous
both method test(boolean...) and method test(int...) match Обойти неоднозначность можно двумя способами: передать явный пустой массив нужного типа — test(new int[0]) — или отказаться от одной из перегрузок и дать методам разные имена.
Varargs и дженерики: @SafeVarargs
Если varargs-параметр имеет обобщённый тип, компилятор предупреждает о возможном «загрязнении кучи» (heap pollution): из-за стирания типов массив List<String>[] на самом деле является List[], и в него можно положить что угодно.
static void unsafe(List<String>... lists) { // предупреждение: possible heap pollution
Object[] objects = lists;
objects[0] = List.of(1, 2, 3); // компилятор не возражает
String s = lists[0].get(0); // ClassCastException во время выполнения
} Если метод только читает элементы и не записывает их в массив, предупреждение снимают аннотацией @SafeVarargs:
@SafeVarargs
static <T> List<T> toList(T... items) {
return new ArrayList<>(Arrays.asList(items));
} Где можно ставить @SafeVarargs
Аннотация допустима только для методов, которые нельзя переопределить: static, final, конструкторов, а начиная с Java 9 — ещё и private. На обычном методе экземпляра она вызовет ошибку компиляции, потому что наследник мог бы нарушить обещанную безопасность.
Varargs в стандартной библиотеке
Varargs используется в JDK повсеместно, и знакомые методы выглядят «магией» именно благодаря ему:
System.out.printf("%s = %d%n", "age", 30); // printf(String, Object...)
String s = String.format("%s-%s", "a", "b"); // format(String, Object...)
List<String> list = List.of("a", "b", "c"); // of(E...)
List<Integer> nums = Arrays.asList(1, 2, 3); // asList(T...)
int[] copy = {1, 2, 3};
System.out.println(Arrays.toString(copy)); На чём чаще всего ошибаются
- Передача
nullнапрямую. Вызовtest(null)для методаtest(String... s)передаёт не «один элемент null», аnullвместо всего массива — следующий жеs.lengthброситNullPointerException. Чтобы передать один элемент, пишитеtest((String) null). - Примитивный массив вместо объектного.
Arrays.asList(new int[]{1, 2, 3})вернётList<int[]>размером 1, а не список из трёх чисел:int[]— это один объект для varargs типаT. НуженInteger[]илиArrays.stream(...).boxed(). - Ожидание
nullпри пустом вызове. Проверкаif (array == null)бессмысленна, если метод вызывают обычным образом: приходит пустой массив. - Слишком «жадный» varargs. Метод
test(Object... args)подходит почти под любой вызов и легко перехватывает те, которые вы планировали отдать другой перегрузке. - Varargs как замена отсутствующей валидации. Если аргументов всегда должно быть не меньше одного, объявляйте
test(int first, int... rest)— тогда требование проверит компилятор, а неifв рантайме.
Часто задаваемые вопросы
Можно ли передать массив в varargs-метод?
Да. Метод test(int... array) принимает и test(1, 2, 3), и test(new int[]{1, 2, 3}) — компилятор в первом случае сам создаёт массив, так что оба вызова эквивалентны. Обратное неверно: метод, объявленный как test(int[] array), нельзя вызвать через test(1, 2, 3).
Что вернёт array.length, если вызвать метод без аргументов?
Ноль. Компилятор передаёт пустой массив, а не null, поэтому и array.length, и цикл for-each отработают корректно. null внутри метода может оказаться только в одном случае — если его передали явно, например test((int[]) null).
Почему varargs-параметр обязан быть последним?
Иначе компилятор не смог бы определить, где заканчивается список переменной длины и начинаются следующие параметры. По той же причине varargs-параметр в методе может быть только один. Всё остальное объявляется до него: void log(String prefix, Object... values).
Что быстрее: varargs или перегрузки с фиксированными параметрами?
Перегрузки с фиксированными параметрами: varargs создаёт новый массив на каждый вызов. Для прикладного кода разница несущественна, но в библиотеках и горячих циклах её учитывают — например, в JDK у List.of() есть отдельные перегрузки на количество элементов от нуля до десяти.
Зачем нужна аннотация @SafeVarargs?
Она подавляет предупреждение о загрязнении кучи для методов с обобщённым varargs-параметром вида T.... Ставить её можно только там, где метод нельзя переопределить: на статических и final-методах, конструкторах, а с Java 9 — и на private-методах. И только если метод действительно не записывает ничего в этот массив.
Видео объяснение
Предпочитаете видеоформат? Посмотрите этот урок с примерами и объяснениями.
Комментарии