Сегодня мы обсудим такую штуку как varargs в Java. Varargs, или Variable Arguments, это технология, которая позволяет создавать методы с произвольным количеством аргументов. Записывается такой параметр тремя точками после типа,
int... numbers, внутри метода он ведет себя как обычный массив, а при вызове аргументы просто перечисляют через запятую. В данной статье мы научимся создавать такие методы, обсудим, зачем нужны varargs и когда их лучше использовать.
Кратко
- Varargs это параметр переменной длины: три точки после типа,
int... numbers, и метод принимает сколько угодно аргументов этого типа.
- Появились в Java 5. До них выбор был так себе: либо плодить перегрузки, либо требовать от вызывающего готовый массив.
- Внутри метода vararg это обычный массив: у него есть
length, по нему можно пройти циклом.
- Такой параметр у метода может быть только один, и стоять он обязан последним.
- При вызове массив собирать не нужно, аргументы перечисляют через запятую. Готовый массив передать тоже можно.
- В самой Java на varargs построены
System.out.printf и String.format, так что вы ими уже пользовались.
Что такое varargs?
Varargs это аргументы переменной длины: фича, которая появилась еще в JDK 5. Varargs позволяют нам создавать методы с произвольным количеством аргументов. По большому счету, создавать такие методы можно было и раньше. Правда, делать это было не очень удобно.
Приведем пример. Допустим, нам необходимо написать метод, который будет принимать произвольное количество целочисленных аргументов и складывать их вместе.
![Разноцветные пружинки-слинки разной длины на цветной бумаге: наглядная метафора аргументов переменной длины]()
У нас есть два варианта.
Вариант 1. Перегрузка:
class Calculator {
int sum(int a, int b){...};
int sum(int a, int b, int c){...};
int sum(int a, int b, int c, int d){...};
int sum(int a, int b, int c, int d, int e){...};
}
Но с перегрузкой возникают две проблемы. Во-первых, не всегда понятно, когда пора остановиться, а во-вторых, это громоздко. Лучше уж массивы. Про саму перегрузку, если она пока в новинку, есть
отдельный разбор.
Вариант 2. Массивы:
class Calculator {
int sum(int[] numbers){...};
}
С массивом уже не громоздко, и вроде бы массив выглядит ничего так… до тех пор, пока не придет пора вызвать метод:
public static void main(String... sss) {
Calculator calculator = new Calculator();
int[] arguments = new int[7];
arguments[0] = 1;
arguments[1] = 10;
arguments[2] = 123;
arguments[3] = 234;
arguments[4] = 6234;
arguments[5] = 12;
arguments[6] = 8;
int sum = calculator.sum(arguments);
}
Согласны, можно, конечно, и короче все записать. Но во-первых, хочется все-таки продемонстрировать неудобство использования массивов в качестве аргументов переменной длины, а во-вторых, даже если мы запишем так:
int[] arguments = {1,10,123,234,6234,12,8};
int sum = calculator.sum(arguments);
То мы все равно не избавимся от излишнего количества кода.
Однако с выходом Java 5 для решения этих проблем появилась фича Varargs. Она позволила передавать в методы произвольное количество аргументов.
Выглядит это примерно так:
public class Calculator {
public static void main(String... sss) {
Calculator calculator = new Calculator();
int sum = calculator.sum(1,10,123,234,6234,12,8);
}
int sum(int... numbers){
return Arrays.stream(numbers).sum();
}
}
Соберем все три способа в одном месте.
| Способ |
Как объявляется |
Что не так |
| Перегрузка |
int sum(int a, int b), int sum(int a, int b, int c) и так далее |
Непонятно, на каком количестве аргументов остановиться, и объявлений становится много |
| Массив |
int sum(int[] numbers) |
В объявлении компактно, но массив приходится собирать на каждом вызове |
| Varargs |
int sum(int... numbers) |
Собирать ничего не нужно, аргументы перечисляются через запятую прямо в вызове |
Итак, резюмируем. Varargs это аргументы переменной длины, фича, которая появилась с выходом Java 5. Кстати, сумму в примере выше считает
Stream API:
Arrays.stream(numbers) превращает массив в поток чисел, а
sum() складывает их. Далее более подробно рассмотрим некоторые правила работы с Varargs.
5 правил varargs
Правило 1. Vararg-аргумент обозначается тремя точками
Vararg аргумент (или же аргумент переменной/произвольной длины) записывается через троеточие следующим образом:
String... words
Integer... numbers
Person... people
Cat... cats
Правило 2. Varargs бывают только у параметров метода
Аргумент произвольной длины может быть указан только как аргумент некоторого метода:
void print(String... words)
int sum(Integer... numbers)
void save(Person... people)
void feed(Cat... cats)
Правило 3. Внутри метода vararg это массив
Каждый такой аргумент переменной длины в теле метода является массивом:
void print(String... words){
for (int i = 0; i < words.length; i++) {
System.out.println(words[i]);
}
}
Правило 4. Vararg-аргумент идет последним
Vararg аргумент должен быть последним в списке аргументов метода:
void print(String... words, String anotherWord)
void print(String... words, int someNumber)
void print(String anotherWord, String... words)
void print(int someNumber, String... words)
И такой параметр у метода может быть только один.
Спецификация языка говорит об этом прямо: «At most one variable arity parameter is permitted for a method or constructor», а попытка поставить его не на последнее место это ошибка компиляции.
Правило 5. Массив при вызове создавать не нужно
Несмотря на то, что varargs являются массивами, при вызове метода, который принимает аргументы переменной длины, не обязательно создавать массив. Достаточно и даже желательно просто перечислить необходимые аргументы через запятую:
public class Main {
public static void main(String... sss) {
print("Как","же","прекрасно","изучать","Java");
}
static void print(String... words){
for (int i = 0; i < words.length; i++) {
System.out.println(words[i]);
}
}
}
А если не передать ни одного аргумента, метод все равно отработает: внутрь придет пустой массив длины 0, а не
null. Компилятор в этом месте
подставляет создание массива из перечисленных аргументов, и для пустого списка получается
new String[]{}.
Примеры varargs
В примере ниже мы напишем метод, который принимает varargs состоящий из целых чисел и выводит на экран количество переданных элементов и их сумму. Передадим в этот метод и массив, и ряд целых чисел (оба варианта допустимы):
public static void main(String... sss) {
int[] a = new int[100];
for (int i = 0; i < a.length; i++) {
a[i] = i;
}
sum(a);
sum(1,2,3,4,5,6,7,8,9,10);
}
static void sum(int... numbers){
final int length = numbers.length;
final int sum = Arrays.stream(numbers).sum();
final String lineSeparator = System.lineSeparator();
System.out.printf("Кол-во элементов для сложения - %d, сумма - %d%s", length, sum, lineSeparator);
}
После запуска программа выведет:
Кол-во элементов для сложения - 100, сумма - 4950
Кол-во элементов для сложения - 10, сумма - 55
Стоит сказать, что метод
System.out.printf также принимает varargs. Если мы взглянем на код этого метода, то убедимся в этом:
public PrintStream printf(String format, Object ... args) {
return format(format, args);
}
Обратите внимание на пробел в
Object ... args. Троеточие это отдельный токен, поэтому пробел между ним и типом язык разрешает, хотя спецификация и советует так не писать. В следующем листинге тот же самый параметр записан слитно, и разницы между этими двумя записями нет никакой.
Еще один широко используемый метод, принимающий varargs, это
String.format. Его код приведен ниже:
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
Подводные камни varargs
Пять правил выше описывают, как varargs устроены. А теперь три места, где они ведут себя не так, как ожидает читатель кода.
null вместо массива
Возьмем метод из правила 5 и вызовем его вот так:
static void print(String... words){
for (String word : words) {
System.out.println(word);
}
}
print(null);
Кажется, что внутрь придет массив из одного элемента, равного
null. На самом деле придет
null вместо самого массива, и цикл упадет с
NullPointerException. Причина в правиле из спецификации: массив собирается только тогда, когда последний аргумент нельзя присвоить массиву напрямую. А
null переменной типа
String[] присвоить можно, поэтому он и уходит в метод как готовый массив.
Компилятор это место замечает и выдает предупреждение «non-varargs call of varargs method with inexact argument type for last parameter», предлагая привести аргумент к
String для обычного varargs-вызова или к
String[], если массив-
null нужен намеренно.
Массив, который разворачивается, и массив, который нет
Тот же механизм дает второй сюрприз, если передавать в varargs готовый массив:
Object[] objects = {"раз", "два"};
System.out.printf("%s %s%n", objects);
int[] numbers = {1, 2};
System.out.printf("%s%n", numbers);
В первом случае
Object[] присваивается параметру
Object... args напрямую, поэтому массив становится списком аргументов и в строку подставляются оба элемента. Во втором случае
int[] параметру типа
Object[] присвоить нельзя: массив примитивов не является массивом объектов. Поэтому компилятор заворачивает его целиком, в
new Object[]{numbers}. Аргумент получается один, и печатается не содержимое массива, а результат его
toString(), то есть что-то вроде
[I@1b6d3586.
Новый массив на каждый вызов
Vararg-параметр это не бесплатный синтаксический сахар.
Спецификация описывает вызов так, как будто вместо перечисления аргументов программист сам написал
new T[]{...} с этими элементами. То есть массив действительно создается заново на каждом вызове.
Один раз это ничего не значит, а вот в цикле на миллионы итераций лишние объекты в куче становятся заметны. В стандартной библиотеке эту цену иногда платить не хотят: рядом с
List.of(E... elements) объявлены отдельные перегрузки на ноль, один, два и так далее до десяти элементов, и varargs-версия включается только начиная с одиннадцатого. Правда, это ровно тот случай перегрузки, от которого предостерегает следующий раздел, так что в обычном прикладном коде так делать не стоит: прием оправдан там, где метод вызывают миллиарды раз.
Когда использовать varargs?
Ответ на этот вопрос зависит от того, кто спрашивает.
Если подобный вопрос задает клиент некоторого API, в котором есть методы с varargs, то ответ будет «использовать такие методы как можно чаще». Для клиента кода varargs существенно упрощает жизнь, облегчая написание кода и повышая его читаемость.
Однако в случае, если данный вопрос задает разработчик API, который интересуется, как часто стоит создавать методы с varargs, то ответ будет «не следует часто использовать varargs». Varargs следует использовать только тогда, когда выгода от его использования очевидна.
Также не следует перегружать методы с varargs, так как это вызовет у клиентов вашего кода затруднения в понимании, какой из перегруженных методов в действительности вызывается. Отдельная история это varargs с дженериками: там компилятор предупреждает о возможном засорении кучи, и разбирается это в
отдельной статье.
Заключение
Итак, мы разобрали еще одну тему, varargs в Java. Разобрали, что это такое. Расписали правила использования varargs. Взглянули на примеры методов с аргументами произвольной длины, а также обсудили, когда varargs лучше использовать, а когда от использования лучше воздержаться.
В качестве домашнего задания можно реализовать следующие методы:
- Написать
void метод, который принимает ряд целых чисел и выводит на экран их среднее арифметическое.
- Написать
void метод, который принимает ряд строк и выводит на печать самое длинное слово.
- Написать метод, который возвращает
boolean и принимает первым аргументом переменную типа String, а следующим аргументом ряд boolean, переменной длины.
Возможные значения String: AND, OR, XOR. В случае, если первый аргумент имеет какое-то другое значение, метод должен бросить исключение IllegalArgumentException.
Метод должен осуществить логическую операцию (указанную в первом аргументе) над каждым элементом varargs аргумента и вернуть результат вычисления.
Пример аргументов и результата:
- "AND", true, true, false: вернет false
- "OR", false, true, false, false: вернет true
Вопросы и ответы
Что такое varargs в Java?
Varargs, они же аргументы переменной длины, это способ объявить метод, который принимает любое количество аргументов одного типа. Появился он в Java 5. В объявлении такой параметр помечают тремя точками после типа, например int... numbers, а при вызове аргументы просто перечисляют через запятую.
Как объявить метод с varargs?
Поставьте три точки между типом и именем параметра: void print(String... words). Дальше метод пишется как обычно. Вызывать его можно и с одним аргументом, и с десятью, и вообще без аргументов, менять объявление под каждый случай не нужно.
Чем varargs отличается от массива в параметрах метода?
Внутри метода отличий нет: vararg это тот же массив, у него есть length и по нему можно пройти циклом. Разница на стороне вызова. Методу с массивом нужно передать готовый массив, а методу с varargs достаточно перечислить значения через запятую, массив за вас соберет компилятор. При этом готовый массив в varargs-метод передать тоже можно.
Сколько varargs-параметров может быть у метода?
Только один, и он обязан стоять последним в списке параметров. Так требует спецификация языка: at most one variable arity parameter is permitted for a method or constructor. Объявление вроде print(String... words, int number) просто не скомпилируется.
Что придет в метод, если вызвать его вообще без аргументов?
Пустой массив длины 0, а не null. Компилятор на месте вызова собирает массив из перечисленных аргументов, и если их ноль, получается массив нулевой длины. Поэтому цикл по такому параметру просто не сделает ни одной итерации, а обращение к length вернет 0.
Когда varargs лучше не использовать?
Если вы пишете код, которым будут пользоваться другие, заводите varargs только там, где выгода очевидна. Не стоит перегружать методы с varargs: читателю кода будет трудно понять, какая версия вызывается. Отдельно осторожно с дженериками: varargs из параметризованных типов вызывают предупреждение компилятора о возможном засорении кучи.
Читайте также
voidметод, который принимает ряд целых чисел и возвращает их среднее арифметическое." Шта?!