Как посчитать среднее арифметическое в Java

Как посчитать среднее арифметическое в Java Полезное

Среднее арифметическое — это сумма всех чисел набора, деленная на их количество. В 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, и дальше все как с массивом.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)