Java Tips | コレクション:平均算出

Java Java
スポンサーリンク

「平均算出」は“バラバラな値を代表する一つの数字”を作る技

平均は、たくさんの値を「代表する一つの数字」にまとめるための道具です。
売上の平均金額、テストの平均点、作業時間の平均、単価の平均…。

業務では「なんとなく平均を出す」のではなく、
空データ・小数点・丸め・null の扱いなどをきちんと決めたうえで、
毎回同じルールで計算できるようにしておくことがとても大事です。


基本形:数値 List の平均を出す

Stream を使った最小限の書き方

まずは、List<Integer> の平均を出す一番シンプルな形から見てみます。

import java.util.List;

public class AverageBasic {

    public static void main(String[] args) {
        List<Integer> scores = List.of(70, 80, 90);

        double avg = scores.stream()
                           .mapToInt(Integer::intValue)
                           .average()
                           .orElse(0.0); // 空なら 0.0 とする

        System.out.println(avg); // 80.0
    }
}
Java

ここで重要なのは二つです。

一つ目は、mapToInt で「int 専用ストリーム」に変換してから average() を呼んでいることです。
average()OptionalDouble を返すので、「値がない(空)」という状態を表現できます。

二つ目は、orElse(0.0) で「空だったときの平均値」を決めていることです。
ここをどうするか(0.0 にするのか、NaN にするのか、例外にするのか)は、
プロジェクト全体でルールを決めておくとブレません。


平均算出をユーティリティメソッドにまとめる

「null や空 List の扱い」を一箇所に閉じ込める

同じような平均計算を何度も書くなら、
ユーティリティにしてしまうとコードがかなりスッキリします。

import java.util.List;

public final class Averages {

    private Averages() {}

    public static double averageInt(List<Integer> source) {
        if (source == null || source.isEmpty()) {
            return 0.0;
        }
        return source.stream()
                     .mapToInt(Integer::intValue)
                     .average()
                     .orElse(0.0);
    }
}
Java

使い方はこうなります。

List<Integer> scores = List.of(70, 80, 90);

double avg = Averages.averageInt(scores);
System.out.println(avg); // 80.0
Java

ここでの重要ポイントは、「null と空 List の扱いをユーティリティ側で決めている」ことです。
呼び出し側は「平均が欲しい」とだけ考えればよく、
毎回 null チェックや OptionalDouble の扱いを書かなくて済みます。


オブジェクト一覧から「特定の項目の平均」を出す

例:商品一覧から「価格の平均」を出す

業務では、単なる List<Integer> よりも、
「オブジェクトの特定のフィールドの平均」が欲しいことがほとんどです。

class Item {
    private final String name;
    private final int price;
    public Item(String name, int price) {
        this.name = name;
        this.price = price;
    }
    public String getName() { return name; }
    public int getPrice() { return price; }
}
Java

これを平均するユーティリティは、こう書けます。

import java.util.List;
import java.util.function.ToIntFunction;

public final class Averages {

    private Averages() {}

    public static <T> double averageInt(
            List<T> source,
            ToIntFunction<? super T> mapper
    ) {
        if (source == null || source.isEmpty()) {
            return 0.0;
        }
        return source.stream()
                     .mapToInt(mapper)
                     .average()
                     .orElse(0.0);
    }
}
Java

使い方はこうです。

List<Item> items = List.of(
        new Item("A", 100),
        new Item("B", 200),
        new Item("C", 300)
);

double avgPrice = Averages.averageInt(items, Item::getPrice);
System.out.println(avgPrice); // 200.0
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「ToIntFunction<? super T> mapper が“どの項目を平均するか”を表している」ことです。
Item::getPrice を渡すことで、「価格の平均が欲しい」という意図が明確になります。

二つ目は、「ユーティリティ側は“どう平均を計算するか”だけを知っていて、“何を平均するか”は呼び出し側が決める」構造になっていることです。
これにより、同じメソッドで「価格」「数量」「スコア」など、何でも平均を取れます。

三つ目は、「空や null の扱いを一箇所に閉じ込めている」ことです。
プロジェクト全体で「空なら 0.0」というルールを統一できます。


グルーピングと組み合わせた「グループごとの平均」

例:カテゴリごとの平均価格

平均算出が本領を発揮するのは、「グルーピング」と組み合わせたときです。
例えば、商品を「カテゴリごとの平均価格」にしたい場合。

class Item {
    private final String category;
    private final int price;
    public Item(String category, int price) {
        this.category = category;
        this.price = price;
    }
    public String getCategory() { return category; }
    public int getPrice() { return price; }
}
Java

Stream だけで書くとこうなります。

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

List<Item> items = List.of(
        new Item("食品", 100),
        new Item("食品", 300),
        new Item("雑貨", 400)
);

Map<String, Double> avgByCategory =
        items.stream()
             .collect(Collectors.groupingBy(
                     Item::getCategory,
                     Collectors.averagingInt(Item::getPrice)
             ));

System.out.println(avgByCategory); // {食品=200.0, 雑貨=400.0}
Java

ここでの重要ポイントは、
groupingBy(カテゴリ, averagingInt(価格)) という一行が、
“カテゴリごとに価格の平均を出す”という業務仕様をそのまま表している」ことです。

「まず Map に詰めてから for で回して合計と件数を…」という手作業より、
はるかに読みやすく、バグも入りにくくなります。


小数点と丸めの扱いをどうするか

表示用には BigDecimal や丸めルールを意識する

average()averagingInt()double を返します。
内部的には「合計 ÷ 件数」をしているだけなので、
表示するときに「小数第何位まで出すか」「四捨五入か切り捨てか」を決める必要があります。

例えば、「小数第2位で四捨五入して表示したい」場合。

double avg = Averages.averageInt(items, Item::getPrice);

double rounded = Math.round(avg * 100.0) / 100.0;
System.out.println(rounded);
Java

金額などで厳密な丸めが必要な場合は、BigDecimal を使うこともあります。

import java.math.BigDecimal;
import java.math.RoundingMode;

BigDecimal avgPrice =
        BigDecimal.valueOf(avg)
                  .setScale(2, RoundingMode.HALF_UP);
Java

ここでの重要ポイントは、
「平均そのもの(double)と、“どう表示するか(丸めルール)”は別物として考える」ことです。

ユーティリティは「正しい平均値」を返すところまでにしておき、
丸めやフォーマットは表示層で行う、という分離を意識すると設計がきれいになります。


null を含むデータの平均算出

「null を 0 とみなすか」「そもそも除外するか」を決める

現実のデータには null が混ざります。
平均算出で大事なのは、「null をどう扱うか」を決めることです。

例えば、「null は 0 とみなして平均を出す」なら、こう書けます。

public static double averageIntTreatNullAsZero(List<Integer> source) {
    if (source == null || source.isEmpty()) {
        return 0.0;
    }
    return source.stream()
                 .mapToInt(v -> v == null ? 0 : v)
                 .average()
                 .orElse(0.0);
}
Java

逆に、「null は集計対象から除外する」なら、こうです。

public static double averageIntIgnoreNull(List<Integer> source) {
    if (source == null || source.isEmpty()) {
        return 0.0;
    }
    return source.stream()
                 .filter(v -> v != null)
                 .mapToInt(Integer::intValue)
                 .average()
                 .orElse(0.0);
}
Java

ここでの重要ポイントは、
「null の扱いも“平均算出のルール”の一部」だと意識することです。

ユーティリティに閉じ込めておけば、
呼び出し側は「このメソッドは null をどう扱うのか」を意識せずに済みます。


まとめ:平均算出ユーティリティで身につけてほしい感覚

平均算出は、
単に「合計 ÷ 件数をするテクニック」ではなく、
空・null・小数点・丸め・グルーピングなどを含めて、
「一覧から“代表値”を一貫したルールで取り出すための設計」です。

数値 List なら mapToInt(...).average().orElse(0.0) の形を基本として覚える。
オブジェクト一覧では、「どの項目を平均するか」を関数(Item::getPrice など)で渡す汎用ユーティリティにする。
グルーピングと組み合わせて、「カテゴリごとの平均」「ステータスごとの平均」を一行で書けるようにする。
null や空コレクションの扱いをユーティリティ側で統一し、呼び出し側の思考負荷を減らす。
丸めや表示形式は別レイヤーの責任にして、「平均値そのもの」と切り分けて考える。

あなたのコードのどこかに、
毎回「合計して件数で割る」を手書きしている箇所があれば、
それを一度「平均ユーティリティ+Stream」に置き換えられないか眺めてみてください。

その小さな整理が、
「一覧から“意味のある代表値”を迷いなく取り出せるエンジニア」への、
確かな一歩になります。

タイトルとURLをコピーしました