Java

[java8] StreamAPIを理解するために

未経験でエンジニアに転職し、javaのチームに配属されて、早4ヶ月。
もうjavaにも慣れて良い頃なのに、listの展開で拡張for文を使っている自分がいます。
いいかげんStreamAPIと仲良くなりたいのでまとめてみました。

随時メソッド追加していく予定です。

StreamAPIとは?

概要

最初のとっかかりとしては「なんか配列を手軽に操作できるもの」程度の印象で良いと思います。
以下に遅延評価や、非同期処理など記載しましたが、最初は知らなくても大丈夫です。(自分もあまり理解してないのでそう思いたい..)

StreamAPIの特徴

Streamは配列の操作を少ない記述で行える他、以下のような機能を持っています。

非同期処理

StreamAPIはその非同期処理が行なえます。
非同期処理とは、その名の通り、ある一連の処理を順次実行する(同期す)のではなく、並列し行う(非同期)ことです。
StreamAPIで非同期処理を行う場合は、通常のList.stream()ではなく、list.parallelStream()もしくはList.stream().parallel()を使います。

例えば以下のようなリストの要素を画面に出力するという処理がある場合、、

List<Integer> demoList = Arrays.asList(1, 2, 3, 4, 5);

同期的に行うと、順次実行されるので出力は配列の並び順になります。

    for(Integer i : demoList) {
      System.out.print(i);
    }
    // 12345

ですが、非同期に行うと並列に処理されるので、出力は配列の並び順と異なります。

    demoList.stream().parallel().forEach(System.out::print);
    // 32415

また、非同期処理を行うことで、Forでlistを回すより高速に処理することができます。
実際に時間を計測してみました。

    // 対象List
    List<Integer> demoList = Stream.iterate(1, n -> n + 1).limit(100000).collect(Collectors.toList());

    // For処理前の時刻を取得
    long startTimeFor = System.currentTimeMillis();

    // Forで出力
    for(Integer i : demoList) {
      System.out.println(i);;
    }

    // For処理後の時刻を取得
    long endTimeFor = System.currentTimeMillis();

    // Stream処理前の時刻を取得
    long startTimeStream = System.currentTimeMillis();

    // Streamで出力
    demoList.parallelStream().forEach(System.out::println);

    // Stream処理後の時刻を取得
    long endTimeStream = System.currentTimeMillis();

    System.out.println("処理時間For: " + (endTimeFor - startTimeFor) + " ms");
    System.out.println("処理時間Stream: " + (endTimeStream - startTimeStream) + " ms");

結果は以下のとおりです。思ったより差がでませんが、上記処理では平均して100ms程早かったです。

処理時間For: 686 ms
処理時間Stream: 573 ms

遅延評価

StreamAPIでは関数型の言語が持つ遅延評価を行えます。
遅延評価とは、、

プログラミングにおいて、式は出てきた時点で計算されることが多いが、それをあえて実際に必要になるまで計算しないでおくことを遅延評価という。

by ニコニコ大百科|遅延評価

とのです。
StreamAPIでは、後述の中間操作(mapやfilter)の時は式が評価されません。終端操作を実行した時初めて、評価されます。
実際に動かしてみて検証してみましょう。

    // 対象List
    List<Integer> demoList = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));

    // 偶数のみStream化 中間処理なのでまだ評価されてない。
    Stream<Integer> oddListStream = demoList.stream().filter(n -> n % 2 == 0);

    // demoListに要素を追加
    demoList.add(6);
    demoList.add(7);
    demoList.add(8);

    // Streamを出力
    oddListStream.forEach(System.out::print);

普通に考えると、Streamを生成しているoddListStreamには2,4が入ってるはずです。
しかし、Streamの中間操作は遅延評価されるので、終端操作のforEachが呼ばれるまで評価されません。
なので、出力は、、、

$  2468

と、Stream生成後にdemoListに追加した6,7,8が処理されています。

処理の流れ

ここから実際にStreamの動作を生成→中間操作→終端操作の順で見ていきます。

1. Streamの生成

まず、後述する操作を行うための基本となるStreamを生成します。
Streamはよく使われるList.stream()以外にも、コンストラクタを使う方法やbuilderを使う方法など様々あります。

    // list型から
    Stream<Integer> numberStreamFromList = Arrays.asList(8, 1, 4, 2, 9).stream();

    // Stream.ofを使って
    Stream<Integer> numberStreamFromStreamOf = Stream.of(8, 1, 4, 2, 9);

    // builderを使って
    Builder<String> builder = Stream.builder();
    Stream<String> stringStream = builder.add("hoge").add("fuga").build();

    // 空のStream
    Stream<String> emptyStream = Stream.empty();

    // iterateを使って
    Stream<Integer> numberStreamFromIterate = Stream.iterate(1, n -> n * 10).limit(5);

iterateを使う方法だけ少しわかりにくいので補足です。
基本構文は以下になります。

Stream.iterate(初期値, 初期値が入る変数 -> 変数に対する処理).limit(繰り返し回数)

なぜlimitをかけているかというと、limitを使わない場合無限に生成されてしまうからです。

iterarteを使えばこんな出力が簡単に行えます。

1から数字を言って3の倍数のときだけアホになる(時代を感じる..)

    Stream.iterate(1, n -> n + 1).limit(100)
        .map(n -> n % 3 == 0 ? "アホ" : n)
        .forEach(System.out::println);

2. 中間操作

中間操作とは、配列の各要素に対して、フィルタリングや、関数の実行などの処理を行うことです。
中間操作は何度も繰り返すことが出来ます。filterしたあとmapで処理するなど。
中間操作の戻り値は、stream型となりますので注意してください。
rubyのようにlist.mapで配列を返してはくれません。
stream型以外にするためには、後述の終端操作が必要です。

要素の選択 [filter]

filter()を利用することで、要素をある条件でフィルタリングすることができます。
以下使用例です。

配列から偶数のみ出力する

    // 対象リスト
    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    // 偶数のみ出力 for文
    for (Integer number : numberList) {
      if (number % 2 == 0) {
        System.out.println(number);
      }
    }

    // 偶数のみ出力 Stream
    numberList.stream()
        .filter(n -> n % 2 == 0)
        .forEach(System.out::println);

要素の編集 [map]

配列の各要素を2倍して出力する

    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    // 各要素を2倍して出力 for文
    for (Integer number : numberList) {
      System.out.println(number * 2);
    }

    // 各要素を2倍して出力 Stream
    numberList.stream()
        .map(n -> n * 2)
        .forEach(System.out::println);

要素の並び替え [sort]

要素の並び替えもsortを使えばラクラクです。

    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    List<Integer> sortListAsc = numberList.stream().sorted().collect(Collectors.toList());

    List<Integer> sortListDesc = numberList.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

    System.out.println(sortListAsc);
    // [1, 2, 4, 8, 9]

    System.out.println(sortListDesc);
    // [9, 8, 4, 2, 1]

3. 終端操作

終端操作は、map、filter等で加工したstreamを使用して関数を逐次実行したり、指定の型に変換したりすることができます。
中間操作との違いは、戻り値がStreamではないということです。

以下代表例です。

結果を指定の型で受け取る [ collect ]

collectを使うことで様々な型にStreamを変換して結果を取得することができます。

配列で受け取る [ Collectors.toList() ]

要素を2倍して結果を配列で受け取る

    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    List<Integer> doubleList = numberList.stream()
        .map(n -> n * 2)
        .collect(Collectors.toList());

    System.out.println(doubleList); //[16, 2, 8, 4, 18]

Mapで受け取る [ Collectors.toMap() ]

値を判定して, [値, "偶数"or"奇数"] というMapを作る。

    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    Map<Integer, String> demoMap  = numberList.stream()
        .collect(Collectors.toMap(
            n -> n,
            s -> s % 2 == 0 ? "偶数" : "奇数"));

    System.out.println(demoMap); //{1=奇数, 2=偶数, 4=偶数, 8=偶数, 9=奇数}

要素にそれぞれに対して処理を行う [ forEatch ]

そのまま標準出力

    List<Integer> numberList = Arrays.asList(8, 1, 4, 2, 9);

    // 両方同じ
    numberList.stream().forEach(n -> System.out.println(n));
    numberList.stream().forEach(System.out::println);

参考

http://www.task-notes.com/entry/20150518/1431918000
https://qiita.com/nmby/items/52d1b0e2dad5df475737
http://aoking.hatenablog.jp/entry/20110810/1312970979
https://qiita.com/kumazo/items/104fa685da8705b8cfd8#36-flatmap