Один метод, любое число аргументов
Иногда вы заранее не знаете, сколько аргументов понадобится методу. В один момент вам нужно max(3, 9), в следующий — max(1, 7, 4, 2, 8). Без varargs пришлось бы писать перегрузку под каждое число аргументов или заставлять вызывающий код строить массив. Varargs — сокращение от variable arguments (переменные аргументы) — позволяют одному методу принимать ноль, один или много значений одного типа.
Их объявляют, поставив три точки (...) после типа параметра:
Вызывающий код передаёт разрозненные значения; внутри метода numbers — настоящий массив. В этом вся суть: компилятор упаковывает аргументы в массив за вас.
Varargs — это просто массив
Во время выполнения никакой магии нет. Параметр, объявленный как int... numbers, внутри метода — это в точности int[]. Вы можете узнать его length, обращаться по индексу и перебирать его, как любой другой массив.
Поскольку это по-настоящему массив, методу с varargs можно передать и уже имеющийся у вас массив — он принимает обе формы:
Правила: последний и только один
Два правила исключают неоднозначность varargs, и компилятор соблюдает оба:
- Параметр varargs должен быть последним. Сначала идут обычные параметры, затем параметр
...вбирает всё, что осталось. - У метода может быть не более одного параметра varargs. Два сделали бы невозможным понять, где заканчивается один и начинается следующий.
Это компилируется. Но поставить varargs первым — static void log(Object... values, String level) — ошибка компиляции: varargs parameter must be the last. Обязательный параметр level всегда предшествует ....
Где вы уже используете varargs
Вы вызывали методы с varargs всё это время. System.out.printf и String.format — классические примеры: их второй параметр — Object... args:
List.of(...), Arrays.asList(...) и многие API в стиле билдеров тоже используют varargs. Именно поэтому им можно передать любое число элементов, не оборачивая ничего в массив самостоятельно.
Varargs и перегрузка
Varargs и перегрузка взаимодействуют одним неожиданным образом. Когда обычная перегрузка и перегрузка с varargs обе могли бы подойти к вызову, Java предпочитает более конкретную — побеждает метод с фиксированным числом аргументов:
pick(1, 2) подходит обеим, но Java выбирает перегрузку без varargs, потому что она ближе по соответствию. Метод с varargs срабатывает только тогда, когда не подходит ни одна перегрузка с фиксированным числом аргументов. Обычно это именно то, что нужно, но это означает, что добавление перегрузки с varargs может незаметно изменить, к какому методу разрешается вызов.
Следите за пустыми и null-вызовами
Два подвоха ловят людей. Первый: вызов метода с varargs без аргументов передаёт методу пустой массив, а не null — поэтому numbers.length равно 0, и цикл ничего не делает. Проверка на null для случая без аргументов не нужна:
Второй: передача буквального null неоднозначна и опасна: Java может истолковать count(null) как «весь массив varargs равен null», а не «один элемент null», что затем приведёт к NullPointerException при чтении его length. Если вы действительно имеете в виду единственный элемент null, укажите это явно: count((Object) null).
Далее: классы
Varargs завершают то, что можно делать с параметрами методов, — гибкие списки аргументов поверх перегрузки. До сих пор методы свободно жили в классе с static. Дальше вы увидите, как на самом деле работают классы: объединение данных (полей) и поведения (методов) в собственные типы — это сердце объектно-ориентированной Java.
Часто задаваемые вопросы
Что такое varargs в Java?
Varargs (переменные аргументы) позволяют методу принимать любое число аргументов одного типа. Их объявляют, ставя ... после типа: int sum(int... numbers). Вызывающий код может передать ноль, один или много значений, а внутри метода numbers — обычный массив. Это синтаксический сахар: Java сама собирает разрозненные аргументы в массив.
Может ли метод в Java иметь более одного параметра varargs?
Нет. У метода может быть не более одного параметра varargs, и он должен быть последним в списке параметров — например void log(String level, Object... values). Если бы он не был последним, Java не знала бы, где заканчивается часть переменной длины и начинается следующий параметр. Обычные параметры всегда идут перед параметром ....
В чём разница между varargs и параметром-массивом в Java?
Во время выполнения они почти идентичны: параметр varargs int... внутри метода — это int[]. Разница в месте вызова: с varargs вы пишете sum(1, 2, 3) (разрозненные значения), а с явным параметром-массивом приходится создавать и передавать массив: sum(new int[]{1, 2, 3}). Метод с varargs также принимает массив напрямую, поэтому это более гибкий вариант.