Среднее арифметическое — это сумма всех чисел набора, деленная на их количество. В Java его считают двумя основными способами: ручным циклом по массиву и методом average() из Stream API. Оба дают один и тот же результат, но по-разному ведут себя на граничных случаях. Ниже — рабочие примеры с фактическим выводом и три ловушки, на которых чаще всего спотыкаются новички: целочисленное деление, переполнение суммы и пустой массив.
Содержание
Быстрый ответ: цикл по массиву
Начнем с законченного примера, который можно скопировать и запустить. Считаем среднее для трех чисел, сумму и результат сразу храним в double.
public class Average {
public static void main(String[] args) {
double[] nums = {2, 3, 4};
double sum = 0;
for (double n : nums) {
sum += n;
}
double mean = sum / nums.length;
System.out.println(mean);
}
}
Вывод в консоли:
3.0
Логика простая: в цикле for-each накапливаем сумму, потом делим ее на длину массива nums.length. Тип double у суммы и результата — это не случайность, а страховка от главной ловушки, о которой ниже.
Ловушка 1: целочисленное деление
Самая частая ошибка — считать сумму и делить в типе int. В Java деление int на int дает int: дробная часть просто отбрасывается, без округления. Возьмем числа 1, 2, 4 (сумма 7, среднее должно быть 2.33).
Неверный код:
int[] nums = {1, 2, 4};
int sum = 0;
for (int n : nums) {
sum += n;
}
int mean = sum / nums.length; // 7 / 3
System.out.println("Среднее: " + mean);
Фактический вывод:
Среднее: 2
Java посчитала 7 / 3 как целочисленное деление и вернула 2 вместо 2.33. Дробная часть потеряна еще до присваивания.
Исправление — привести хотя бы один операнд к double перед делением:
double mean = (double) sum / nums.length; // 7 / 3.0
System.out.println("Среднее: " + mean);
Теперь вывод:
Среднее: 2.3333333333333335
Важен порядок: (double) sum / nums.length сначала приводит сумму к double, потом делит. А вот (double) (sum / nums.length) не спасет — деление уже прошло в int, и приводить к double будет нечего, кроме готовой двойки.
Ловушка 2: переполнение суммы
int в Java занимает 32 бита и хранит значения примерно до 2.147 миллиарда (точно Integer.MAX_VALUE = 2147483647). Если чисел много или они крупные, сумма в int переполняется и молча уходит в минус — исключения не будет.
Неверный код:
int[] nums = {2_000_000_000, 2_000_000_000};
int sum = 0;
for (int n : nums) {
sum += n;
}
System.out.println(sum);
System.out.println(sum / nums.length);
Фактический вывод:
-294967296
-147483648
Сумма 4_000_000_000 не поместилась в int и «перекрутилась» через максимум в отрицательное число. Среднее получилось бессмысленным.
Исправление — копить сумму в long (64 бита, до примерно 9.2 квинтиллиона), а делить уже с приведением к double:
long sum = 0;
for (int n : nums) {
sum += n;
}
double mean = (double) sum / nums.length;
System.out.println(mean);
Вывод:
2.0E9
2.0E9 — это 2000000000.0 в экспоненциальной записи, ее Java применяет к большим числам double автоматически. Результат верный.
Способ через Stream API
С Java 8 у примитивных стримов есть готовый метод average(). Для int[] его дает Arrays.stream(...), возвращающий IntStream.
import java.util.Arrays;
import java.util.OptionalDouble;
public class AverageStream {
public static void main(String[] args) {
int[] nums = {1, 2, 4};
OptionalDouble avg = Arrays.stream(nums).average();
System.out.println(avg.getAsDouble());
}
}
Вывод:
2.3333333333333335
Здесь два важных момента. Во-первых, average() возвращает не double, а OptionalDouble — обертку, которая может быть пустой (об этом в следующем разделе). Во-вторых, IntStream.average() накапливает сумму во внутреннем long, поэтому переполнения int, как в ловушке 2, тут не случится — стрим считает крупные суммы корректно сам.
Для массива double[] работает так же, но через DoubleStream:
double[] nums = {1.5, 2.5, 4.0};
double mean = Arrays.stream(nums).average().getAsDouble();
System.out.println(mean);
Вывод:
2.6666666666666665
Оговорка про точность: DoubleStream копит сумму в double, и на очень больших массивах с сильно разными по величине числами возможна потеря точности из-за особенностей плавающей запятой. Для обычных задач это незаметно.
Ловушка 3: пустой массив
Если массив пуст, nums.length равен нулю — и способы ведут себя по-разному. У ручного цикла в int деление 0 / 0 бросает исключение:
Exception in thread "main" java.lang.ArithmeticException: / by zero
А в double то же деление даст не исключение, а специальное значение NaN (не число) — его легко не заметить и передать дальше по коду.
Stream решает это аккуратнее: average() на пустом стриме возвращает пустой OptionalDouble, и можно задать значение по умолчанию через orElse:
int[] nums = {};
double mean = Arrays.stream(nums).average().orElse(0.0);
System.out.println(mean);
Вывод:
0.0
Вызов getAsDouble() на пустом OptionalDouble бросил бы NoSuchElementException, поэтому на потенциально пустых данных берут orElse(...) или сначала проверяют isPresent(). Так пустой ввод не превращается в скрытый NaN или падение программы.
Округление результата
Часто нужно не длинное 2.3333333333333335, а два знака после запятой. Здесь важна локаль: String.format("%.2f", ...) в русской локали поставит запятую, а не точку. Чтобы вывод был предсказуем, локаль указывают явно:
import java.util.Locale;
double mean = 2.3333333333333335;
System.out.println(String.format(Locale.US, "%.2f", mean));
Вывод:
2.33
Форматирование только меняет вид строки и не влияет на исходное число — для дальнейших расчетов используйте само значение double, а не отформатированную строку.
Таблица способов
| Способ | Тип результата | Пустой массив | Переполнение суммы |
|---|---|---|---|
Цикл, сумма в int |
int (теряет дробь) |
ArithmeticException (/ by zero) |
молча переполняется |
Цикл, сумма в long, деление через double |
double |
NaN при делении 0.0 / 0 |
безопасно (long вмещает) |
IntStream.average() |
OptionalDouble |
пустой OptionalDouble |
безопасно (сумма в long) |
DoubleStream.average() |
OptionalDouble |
пустой OptionalDouble |
сумма в double, риск потери точности на огромных данных |
Выводы
- Считать среднее лучше с суммой и делением в
double(или братьStream API) — делениеint / intотбрасывает дробную часть. - Для крупных или многочисленных значений копите сумму в
long, иначеintмолча переполнится и результат станет отрицательным. IntStream.average()сам считает сумму вlong, поэтому от переполненияintзащищает без ручного приведения типов.average()возвращаетOptionalDouble: на пустом массиве беритеorElse(...), а неgetAsDouble(), чтобы не поймать исключение.- Для вывода с фиксированным числом знаков задавайте локаль (
Locale.US), иначе разделитель дробной части зависит от системы.
Где применяется / связь с практикой
Среднее арифметическое — базовая операция в отчетах, аналитике, обработке метрик и в учебных задачах на массивы. Умение выбрать тип данных и обработать граничные случаи — это ровно тот навык, который отличает работающий код от кода «на трех числах из примера». Те же приемы (выбор int/long/double, работа со стримами, обертки вроде Optional) переносятся на любую агрегацию данных.
Освойте тему на практике
Разобраться с типами, массивами и Stream API на практике, с проверкой наставника, помогает курс Java Basic в Otus. Посмотреть формат занятий и темы можно на бесплатных открытых уроках Otus — там разбирают реальные задачи и отвечают на вопросы вживую.
FAQ
Как получить целое среднее, а не дробное?
Округлите результат: Math.round(mean) вернет ближайшее целое (тип long), а приведение (int) mean просто отбросит дробную часть. Выбор зависит от того, нужно вам округление или усечение.
Есть ли в стандартной библиотеке готовый метод среднего без стримов?
Прямого Arrays.average(...) в JDK нет. Простейшие варианты — это ручной цикл или Arrays.stream(nums).average(); отдельные хелперы дают сторонние библиотеки, но для одной операции они избыточны.
Как посчитать среднее для List<Integer>?
Преобразуйте список в примитивный стрим: list.stream().mapToInt(Integer::intValue).average().orElse(0.0). Метод mapToInt дает IntStream, и дальше все как с массивом.



