StreamAPIを使ってみた

【前提条件】

[環境]

【概要】

前回までにStreamAPIの基本的なメソッドを調べてきました。

前回までは個々のメソッド単体で使用していましたが、
今回はいくつかのメソッドを組み合わせて見ようと思います。

【サンプルについて】

今回はある程度StreamAPIの便利な面がわかるものを作ってみます。

ある売り上げ(税抜き)のリストから下記のようなものを集計します

  • 売り上げ(税抜き)が2000円以上のものを対象とする
  • 税込み価格に対して合計、平均、最大、最小、件数を算出する
[売り上げ解析用クラス]

売り上げを解析するため用のクラスを作成します。
2000以上の売り上げかを判定するメソッドと
税込み価格を算出するメソッドを作成します。

public class SalesAnalyse {

    private final int basePrice;

    private final int sales;

    public SalesAnalyse(final int basePrice, final int sales) {

        this.basePrice = basePrice;
        this.sales = sales;
    }

    public boolean isOverBase() {

        return (basePrice < sales);
    }

    public double calculateTargetSale() {

        return sales * 1.08;
    }
}
[集計結果用クラス]

集計結果を保持するためのクラスを作成します。
コンソールに対する出力をフォーマットさせるために作成します。

public class AnalyseResult {

    private final double total;
    private final double average;
    private final double min;
    private final double max;
    private final long count;

    public AnalyseResult(final double total,
            final double average,
            final double min,
            final double max,
            final long count) {

        this.total = total;
        this.average = average;
        this.min = min;
        this.max = max;
        this.count = count;
    }

    public AnalyseResult(final DoubleSummaryStatistics summary) {

        this.total = summary.getSum();
        this.average = summary.getAverage();
        this.min = summary.getMin();
        this.max = summary.getMax();
        this.count = summary.getCount();
    }

    public void writeResult() {

        final String separator = "=========================================";

        System.out.println(separator);

        System.out.println("total : " + total);
        System.out.println("average : " + average);
        System.out.println("min : " + min);
        System.out.println("max : " + max);
        System.out.println("count : " + count);

        System.out.println(separator);
    }
}
[データ作成用メソッド]

追加するデータは別メソッドとして作成します。
SalesAnalyseとは別のクラスに作成します。

    private static List<Integer> createDataList() {

        final List<Integer> salesList = new ArrayList<>();

        salesList.add(1200);
        salesList.add(1900);

        salesList.add(2300);
        salesList.add(2400);
        salesList.add(2600);
        salesList.add(2700);

        return salesList;
    }

【for文を使う】

まずはJava8より前のfor文のコードを見てみます。

    private static AnalyseResult executeNotStream(final List<Integer> salesList) {

        double total = 0;
        double max = 0;
        double min = Double.POSITIVE_INFINITY;
        double average = 0;
        long count = 0;
        for (final int sales : salesList) {

            final SalesAnalyse analyse = new SalesAnalyse(2000, sales); // A

            if (analyse.isOverBase()) { // B

                final double calcResult = analyse.calculateTargetSale(); // C
                total += calcResult; // D
                count++; // D

                if (max < calcResult) { // D

                    max = calcResult;
                }

                if (calcResult < min) { // D

                    min = calcResult;
                }
            }
        }
        average = total / count;// D

        return new AnalyseResult(total, average, min, max, count);
    }

コメントでAとなっている箇所でSalesAnalyseオブジェクトを作成し、分析の準備を行います。
Bで対象の売り上げが集計の条件を満たしているかを判定しています。
Cでは税込み価格を算出しています。
Dでは合計、平均、最大、最小、件数を算出しています。

最終的に集計した結果をAnalyseResultオブジェクトにつめて返却しています。

【StreamAPIを使う】

ではStreamAPIを使った場合のコードを見てみます。

    private static AnalyseResult executeStream(final List<Integer> salesList) {

        final DoubleSummaryStatistics summary =
                salesList.stream()
                        .map(v -> new SalesAnalyse(2000, v)) // A
                        .filter(SalesAnalyse::isOverBase) // B
                        .map(SalesAnalyse::calculateTargetSale) // C
                        .collect(Collectors.summarizingDouble(v -> v)); // D

        return new AnalyseResult(summary);
    }

コメントでAとなっている箇所でSalesAnalyseオブジェクトを作成し、分析の準備を行います。
Bで対象の売り上げが集計の条件を満たしているかを判定し、フィルタリングします。
Cでは税込み価格を算出して、mapでStreamオブジェクトの処理する型を変換します。
DでCollectors#summarizingDoubleを使い、各値を集計します。

【実行してみる】

ということで書いたコードを実行してみます。
実行用のコードは下記のようになります。

[サンプルソース]
public class Sample {

    public static void main(final String[] args) {

        final List<Integer> salesList = createDataList();

        final AnalyseResult notStreamResult = executeNotStream(salesList);
        final AnalyseResult streamResult = executeStream(salesList);

        notStreamResult.writeResult();
        streamResult.writeResult();
    }

    
    private static List<Integer> createDataList() {
        // 省略
    }
    
    private static AnalyseResult executeNotStream(final List<Integer> salesList) {
        // 省略
    }

    private static AnalyseResult executeStream(final List<Integer> salesList) {
        // 省略
    }
}
[実行結果]
=========================================
total : 10800.0
average : 2700.0
min : 2484.0
max : 2916.0
count : 4
=========================================
=========================================
total : 10800.0
average : 2700.0
min : 2484.0
max : 2916.0
count : 4
=========================================

Streamとそうでない場合とで同じ結果が出力されています。

【両者の違い】

今回のサンプルに関してはStreamAPIとfor文の書き方とで
大きく違いが出るようなものにして見ました。

[コードの見栄え]

コードの見栄えと言うところで見ると
StreamAPIを使ったほうが宣言的っぽくて見やすいと思います。
StreamAPIになれていないと見にくいですが・・・
(1ヶ月前の自分なら「このコードよくわからん」となっていたと思います)

    private static AnalyseResult executeStream(final List<Integer> salesList) {

        final DoubleSummaryStatistics summary =
                salesList.stream()
                        .map(v -> new SalesAnalyse(2000, v)) // A
                        .filter(SalesAnalyse::isOverBase) // B
                        .map(SalesAnalyse::calculateTargetSale) // C
                        .collect(Collectors.summarizingDouble(v -> v)); // D

        return new AnalyseResult(summary);
    }

もう一度コードを見ると、
AでSalesAnalyseオブジェクトに変換して、Bで対象をフィルタリング
Cで値を変換して、Dで結果を集計するというのが
明示的でわかりやすいと思います。

既存のfor文のコードを見ると一個一個何をしているのかはわかりますが、
最終的に何がしたいのかと言うのは
StreamAPIを使った場合に比べて読み取りにくいのではないかと思います。

[並行処理]

StreamAPIの場合はparalleメソッドを呼ぶだけで
並行処理がおこなれるようになります。

for文の場合は・・・自分にはわかりませんw

【まとめ】

StreamAPIのメソッドをいくつか組み合わせて使って見ました。
今までのfor文と比べて宣言的に書けること
並行処理を簡単に実装できることは非常に大きなメリットかなと思います。

StreamAPIへの習得と言うところでいくと
使っていればすぐ慣れると思うので、習得難易度は高くないと思います。
私自身も1ヶ月前まではまったくわからなかったのですが、
こんなエントリを書けるようになっていますし。

Java8以降はStreamAPIを使った方が良いなと個人的に思いました。