Руководство по массивам в Java (ч. 2): varargs

Введение
В первой части руководства мы подробно рассмотрели одномерные массивы и операции, применимые к ним. В данной же статье разбирается такая тема, как varargs.
1. Varargs
Varargs (Variable Arguments List, изменяющийся список аргументов) — это способ создания методов, которые могут принимать произвольное количество аргументов одного типа (от нуля и более). Данная возможность появилась в JDK 5.
В книге «Java. Эффективное программирование» Джошуа Блох пишет: «Использование переменного количества аргументов было разработано для метода printf и рефлексии». Также он называет varargs-методы, как variable arity, т. е. методы с переменной арностью (термин из математики, означающий количество передаваемых аргументов).
Давайте на примере printf и разберем данную функцию языка.
Метод printf (аналог одноименного метода из языка C), как и методы print и println, предназначен для вывода сообщений в консоль, но, в отличие от последних, позволяет применять к ним форматирование. Реализации данных методов находятся в пакете java.io в классе PrintStream, но их мы знаем как методы, вызываемые в конструкции System.out.*
Приведем пример реализации printf:
public PrintStream printf(String format, Object... args) {
    return format(format, args);
}
Данный метод принимает следующие аргументы:
  • String format — выводимое на консоль сообщение, включающее параметры форматирования (дескрипторы)
  • Object… args — произвольное количество значений форматируемых переменных
В целом, можно сказать, что инструкции из String format применяются для форматирования аргументов Object… args.
Запись вида Object… args и есть varargs. Существуют, конечно, и другие способы написания: Object …args или даже Object … args, но они не распространены, и их использовать не стоит. При этом три точки после типа указывают, что метод в качестве аргумента может принимать как массив, так и любую последовательность аргументов, записанных через запятую, которая все равно преобразуется в одномерный массив.
Пусть имеется следующий метод:
public void foo(int... args) {}
Мы его вызываем так:
foo(10, 20);
«Под капотом» компилятор на уровне байт-кода неявно заменяет переданную последовательность массивом. По смыслу это равносильно следующей записи:
foo(new int[]{10, 20});
Varargs работает следующим образом: без участия программиста создается массив, размер которого определяется числом передаваемых аргументов, инициализируется ими, а затем передается в метод. Уже в методе аргумент varargs используется как одномерный массив.
Вернемся к методу printf и приведем пример его применения:
System.out.printf("Hello");
System.out.printf("Player %s attempts: ", player.getName());
System.out.printf("%5s %5s %n", "Dec", "Char");
System.out.printf("Число %,d содержит %d %s количество единиц%n", a, b, c);
Как видно из примера, printf может принимать разное количество аргументов: в нашем случае от одного до четырех. Такая гибкость как раз и обеспечивается за счет конструкции Object… args (тип ожидаемых аргументов может быть любым, не обязательно Object).
Реализовать такую универсальность до введения varargs было возможно двумя способами:
  1. Использовать массив в качестве контейнера для передачи в метод разного количества аргументов (зачастую это громоздко):
char[] arr = new char[4];
char[0] = 'J';
char[1] = 'a';
char[2] = 'v';
char[3] = 'a';

print(arr);

void print(char[] arr) {
    // какой-то код
}
можно, конечно, чуть короче:
char[] arr = {'J', 'a', 'v', 'a'};
print(arr);
Или даже так:
print(new char[]{'J', 'a', 'v', 'a'});
Но это не всегда бывает возможно.
2. Реализовать множество перегруженных методов, принимающих различное число аргументов (что не самая плохая идея, в пределах разумного, в чем мы убедимся далее). Но это сильно засоряет класс, приводя программистов в недоумение от большого числа почти одинаковых методов, да и не всегда понятно, какое количество перегруженных методов нужно реализовать и когда стоит остановиться
В качестве примера можно привести метод для передачи игроков в какую-то игру:
void start(Player p1) {}
void start(Player p1, Player p2) {}
void start(Player p1, Player p2, Player p3) {}
void start(Player p1, Player p2, Player p3, Player p4) {}
Как альтернатива подобной многословности как раз и был предложен varargs.
Разработчики Java посчитали, что нужно предоставить возможность создавать методы с заранее неизвестным числом аргументов без использования массивов, делегировав эту задачу компилятору. Подобные упрощения в языках программирования обычно называются синтаксическим сахаром.
Целью создания таких фич, как varargs, является:
  • упрощение кода
  • уменьшение многословности языка
  • облегчение работы программиста
  • удобство
Разберем еще один пример.
Пусть необходимо реализовать метод, который принимает последовательность аргументов типа String и отображает их в консоли. Для демонстрации воспользуемся методом main, используя в качестве принимаемого аргумента varargs. При этом следует отметить, что String[] и String… являются синонимами.
public class VarargsTest {
    public static void main(String... args) {
        System.out.println("Длина массива (количество аргументов) = " + args.length);
        for (String str : args) {
            System.out.print(str + " ");
        }
    }
}
Запустим в консоли программу, введя любой текст после названия класса:
> java VarargsTest.java My name is Max!
Длина массива (количество аргументов) = 4
My name is Max!
Стоит отметить, что если запустить программу без указания дополнительных аргументов, то массив все равно будет создан и передан в метод, при этом его размер будет равен 0. Попробуйте самостоятельно в этом убедиться.
В качестве ограничения любой метод может использовать varargs только в единственном числе и строго последним аргументом.
Приведем примеры неправильного использования varargs:
1. public void foo1(int... nums, String values) {}
2. public void foo2(String... values, int... nums) {}
3. public void foo3(String[] values, ...int nums) {}
Ни один из вариантов не скомпилируется потому, что:
  1. varargs не является последним
  2. два varargs не допускаются в одном методе
  3. … необходимо размещать после типа, а не до него
И еще один пример:
public int howMany(boolean b, boolean... b2) {
    return b2.length;
}
Варианты вызова метода:
A. howMany();
B. howMany(true);
C. howMany(true, true);
D. howMany(true, true, true);
E. howMany(true, {true, true});
F. howMany(true, new boolean[2]);
  • Вариант A не cкомпилируется, потому что в методе не передается хотя бы начальный параметр (boolean b). Тут действует то правило, когда varargs может быть равен 0, но все остальные аргументы обязательно должны быть переданы
  • Вариант B и C правильные, т. к. первый вызов соответствует правилу из предыдущего пункта, а во втором создается массив размером 1
  • Варианты D и F правильные, т. к. передают нужное количество аргументов: начальный и еще два для преобразования в массив varargs размером 2
  • Вариант E не скомпилируется, потому что неверно объявляется массив: должно быть new boolean[]{true, true}
  • Вариант F верный, т. к. передает начальный параметр и массив размером 2 (массивы и varargs являются синонимами)
2. Производительность varargs
Давайте взглянем на методы of из интерфейса List. Не переживайте, что не знаете, что такое интерфейс, а тем более — не знакомы с List. Это сейчас не самая важная информация. Если хотите, можете воспринимать List, как стандартный «класс» Java, который позволяет создавать динамический массив, умеющий увеличивать свой размер по мере необходимости (у обычного массива размер устанавливается один раз — в момент его создания). Все это вы изучите, когда придет время.
А на данный момент нас интересуют методы of интерфейса List. Как оказывается, этих методов много, от чего они, на первый взгляд, выглядят странно:
of()
of(E e1)
of(E e1, E e2)
of(E e1, E e2, E e3)
of(E e1, E e2, E e3, E e4)
of(E e1, E e2, E e3, E e4, E e5)
of(E e1, E e2, E e3, E e4, E e5, E e6)
of(E e1, E e2, E e3, E e4, E e5, E e6, E e7)
of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8)
of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9)
of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10)
of(E... elements)
Где,
  • E — любой тип данных
  • e1, e2, …, eN — аргументы
Вы вполне справедливо можете задаться вопросом: «Если есть varargs-метод (самый последний в списке), какой смысл иметь еще 11 перегруженных методов с разным количеством принимаемых аргументов?».
Такое количество, казалось бы одинаковых методов, реализовано из-за требований к производительности — методы без varargs зачастую работают быстрее из-за того, что они не имеют накладных расходов на создание массива, инициализацию и сборку мусора, которые возникают при использовании varargs. Одним словом, прямая передача нужного числа аргументов позволяет избежать создания избыточного массива.
Авторы Java собрали статистику и прикинули, какое количество аргументов чаще всего могли бы передавать программисты. И помимо varargs, реализовали метод of с разным количеством аргументов.
Джошуа Блох в своей книге также пишет: «Соблюдайте осторожность при использовании varargs в критических в отношении производительности ситуациях. Каждый вызов метода с varargs требует создания массива и его инициализации».
Как разработчик API вы должны использовать их с осторожностью и только тогда, когда польза действительно убедительна.
НО! Раз уж речь зашла про оптимизацию, то недурно будет процитировать корифеев программирования, которые когда-то высказались на этот счет.
Майкл Джексон об оптимизации:
  • Не оптимизируйте
  • Пока не оптимизируйте (только для экспертов)
Дональд Кнут:
«Программисты тратят огромное количество времени, думая или беспокоясь о скорости некритических частей своих программ, и эти попытки повысить эффективность на самом деле имеют сильное негативное влияние, когда речь идет об отладке и обслуживании. Мы должны забыть о малой эффективности, скажем, примерно в 97% случаев: преждевременная оптимизация — корень всех зол».
Заключение
Использование varargs позволяет вызывать метод с переменным числом аргументов без явного создания массива.
Правила использования varargs:
  • Переменная varargs — это обычная переменная со своим синтаксисом: между типом и названием ставятся три точки
  • Кроме varargs, у метода могут быть и другие аргументы. Но varargs обязательно должен стоять последним в списке аргументов: после него не может быть ни одного аргумента, но перед ним — сколько угодно
  • У метода может быть только один аргумент varargs
Оцените статью, если она вам понравилась!