Java

Java あなたはエセストリームAPI使いになっていないか?

Java SE8 から登場した ストリームAPI というものがあります。

正しく使用できれば非常に便利である一方で、ストリームAPI は関数型プログラミングに基づいたパラダイムであるため、既存のJavaの書き方とは一風違った記述をする必要があります。

謎の矢印(->)が出てきたり、奇妙なコロコロ(::)が出てきたりと、初見ではなかなかにカオスです。

そのこともあり、ストリームAPI恐怖症の人も世の中には存在します。

しかし、そこから少し抜け出してちょっとストリームAPIを使ってみようかなと言う人が、エセストリームAPI使いになってしまうケースがあります。

本記事では、ストリームAPIの誤った使い方を解説し、世の人がエセストームAPI使いにならないように、また抜け出せることを目指していきます。

こんなストリームの使い方は間違っている forEachの罠

駆け出しストリームAPI使いの方が最初に覚えるものが、終端処理の「forEach」です。

実はこれこそがエセストリームAPI使いを生み出してしまう存在なのです。

以下は普通にforEachを使用した場合の書き方です。

import java.util.ArrayList;
import java.util.List;
import static java.util.stream.Collectors.*;

public class ForEachTest {
    
    public static void main(String[] args) {
        // 単語リスト生成
        List<String> wordsList = new ArrayList<>();
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("食パン");

        // 初歩的な使い方
        wordsList.stream().forEach(System.out::println);
    }

}

実行結果は以下のようになります。

乾パン
乾パン
乾パン
食パン

forEachを使用することで、for文のようなことができます。

駆け出しストリーマーはfor文と同じように使うことができるので、とりあえず今まで普通のfor文を使っていたケースでストリームAPIのforEachを使いだします

例えば以下のように、単語リストからキーが単語、バリューが単語の数であるMapを生み出したい場合に、エセストリーム使いはこの様な書き方をします。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.*;

public class ForEachTest {
    
    public static void main(String[] args) {
        // 単語リスト生成
        List<String> wordsList = new ArrayList<>();
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("食パン");

        wordsList.stream().forEach(System.out::println);

        // エセストリーム やってはいけない!
        Map<String, Long> wordsCount1 = new HashMap<>();
        wordsList.stream().forEach(word -> {
            wordsCount1.merge(word, 1L, Long::sum);
        });
        System.out.println(wordsCount1); // {食パン=1, 乾パン=3}

    }

}

一見すると、ストリームAPIを使っているし、謎の矢印や奇妙なコロコロも出てきているし、期待通りの結果も取得されているので、問題ないじゃんと思われるかもしれませんが、これはストリームAPIの皮を被ったエセストリームです。

終端処理「forEach」はstream()の結果を表示する目的で使用されるべきであり、よその変数などに影響を与えてはいけません。

副作用

ストリーム操作の動作パラメータでの副作用は一般にお薦めできません。そのような副作用はしばしば、ステートレス要件への無意識の違反や、スレッドの安全性を脅かすその他の危険につながる可能性があるからです。

https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/package-summary.html

これは本家公式のJavadocにも記載がありますし、Effective Javaにも同様の記載があります。

とりあえずforEachという簡単で容易な選択は、誤った使用方法に繋がりやすいので注意が必要です。

正しくストリームAPIを使用するには?

正しい方法でストリームAPIを使用するためには、ある程度使い方を覚えておく必要があります。

例えば先程の例で言えば以下のように書き換えることができます。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.util.stream.Collectors.*;

public class ForEachTest {
    
    public static void main(String[] args) {
        // 単語リスト生成
        List<String> wordsList = new ArrayList<>();
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("乾パン");
        wordsList.add("食パン");

        wordsList.stream().forEach(System.out::println);

        // エセstream() やってはいけない!
        Map<String, Long> wordsCount1 = new HashMap<>();
        wordsList.stream().forEach(word -> {
            wordsCount1.merge(word, 1L, Long::sum);
        });
        System.out.println(wordsCount1); // {食パン=1, 乾パン=3}

        // 正しいstream()の使い方
        Map<String, Long> wordsCount2 = 
            wordsList.stream()
                     .collect(groupingBy(word -> word, counting()));
        System.out.println(wordsCount2); // {食パン=1, 乾パン=3}
    }

}

終端処理「collect」を使用することで、Mapを生成しています。

collect()はforEachとは異なり、CollectionやStringBuilderに値を蓄積する可変リダクションをするために存在するため、collect()を使用して、Mapを生成するのは正しい書き方です。

このようにストリームAPIには、何らかの操作をするために適切なものが用意されているので、それを正しく選択して使用することが大切です。

正しく使用することができれば、エセストリーム使いにはならなくて済みますし、他の人がエセであれば、しっかりと指摘してあげることができます。

おまけ

Javaにはstaticインポートというインポートをすることで、staticアクセスする際に非修飾で済むようになる技があります。

import static java.util.stream.Collectors.*;

先程の例だと、上記がstaticインポートです。Collectorsをstaticインポートしています。

staticインポートをすると、以下のようにCollectors.と書かなければいけない箇所を書かなくて済むようになります。

// staticインポートをしない場合
wordsList.stream()
         .collect(Collectors.groupingBy(word -> word, Collectors.counting()));

// staticインポートをする場合
wordsList.stream()
         .collect(groupingBy(word -> word, counting()));

一見便利なstaticインポートですが、公式ではほとんどの場面で使用する必要はありませんと記載があります。

しかし、今回staticインポートした、Collectorsクラスについては、数少ないstaticインポートを推奨されたクラスとなっています

Collectorsという単語は少々長いというところと、1行に複数回出てくる可能性があるので、staticインポートをすることで可読性を良くするという目的があるためです。

まとめ

ストリームAPIには、何らかの操作をするために都度適切なものが用意されています。

エセストリーム使いにならないためにも、操作に適した処理を選択して使用することが大切です。

forEachだけを乱用している人は、ストリームAPIの恩恵を受けることができていないので、まずはcollectなどできることを増やしていきましょう。

【お知らせ 無料!】未経験エンジニアがJavaでWebサイトを作成できるようになるための学習ロードマップを、無料で公開しています!

実体験に基づいて作成されているので、プログラミングスクールなどで指導されるロードマップにも劣らない品質です。

こちらの「【Java】エンジニア未経験者がJavaを効率的に勉強する手順を紹介します」リンクから無料で閲覧できるので、是非ご覧ください!

【2021/6 更新】エンジニア未経験者がJavaを効率的に勉強する手順を紹介しますプログラミングを勉強する際に候補に出てくる言語に Java があります。 最近は Java 以外の言語である、Ruby や Pyt...