Java
StreamAPI

Java StreamAPIのメソッドを色々試す(今さら)

Java8で追加されたStreamAPI。
必要に応じて最低限のことを調べて使っていたものの、よくわかっていなかったので、調べ直したり試したりしてみました。

そもそもStreamAPIって?

「要素のストリームに対する関数型の操作をサポートするクラス」とのこと。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/package-summary.html
…自動翻訳なのか、公式のドキュメントはどーも読みづらいです。
英語読んだ方がわかるかもですね…。

ここで言う「要素」は、主にList等のCollection要素を指しているようです。
ストリームっていうのは、こちらの記事に書いてある通り。
https://qiita.com/castaneai/items/3dbdd8543b020aa903cb

StreamAPIって何の役に立つわけ?

私の解釈ですが、

  • Collection要素に対する操作がやりやすい
  • 並列処理できるためマルチコアCPUを活かせる

StreamAPIの概要

StreamインターフェースのJavaDocが全てのベースになりそうだと感じましたので、ここに狙いを絞ります。
https://docs.oracle.com/javase/jp/8/docs/api/java/util/stream/Stream.html

処理の流れ

  1. 1つのソース(Listとか)からStreamを生成
  2. 0〜n個の中間操作
  3. 1つの終端操作

スクリーンショット 2018-12-05 6.06.52.png

ポイント(JavaDocより抜粋)

  • 非干渉でなければいけない(ストリームのソースを変更しない)
  • ほとんどの場合、ステートレスでなければいけない(その結果は、ストリーム・パイプラインの実行中に変化する可能性のあるどの状態にも依存すべきでない)。

とりあえず実験だ!

IntStreamを返すメソッド等、派生モノは除いています。

っとその前に

テストデータ作成用にBeanとメソッドを1つずつ作りました。
mainメソッドからそれを呼び出して、listという変数を作ってから実験をしています。

Bean
public class TestBean {
    String key;
    String value;
    String description;
    public TestBean(String key, String value, String description) {
        this.setKey(key);
        this.setValue(value);
        this.setDescription(description);
    }
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public String getValue() {
        return value;
    }
    public void setValue(String value) {
        this.value = value;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    @Override
    public String toString() {
        return "key:" + this.key + " value:" + this.value + " description:" + this.description;
    }
}
テストデータ作成メソッド
    private static List<TestBean> editTestData() {
        List<TestBean> list = new ArrayList<>();
        list.add(new TestBean("B", "ぶたまん", "豚肉が入っています"));
        list.add(new TestBean("A", "あんまん", "あんこが入っています"));
        list.add(new TestBean("C", "ちゅうかまん", "あんまんぶたまん等の総称です"));
        return list;
    }

中間操作

filter

その名の通りフィルタリングしてくれました!

filter
list.stream()
.filter(x -> x.getValue().equals("あんまん"))
.forEach(x -> System.out.println(x.getDescription()));
実行結果
あんこが入っています

map

Streamを基に別のStreamを作るもの、と解釈しました。
この例では、TestBean型で構成されたStreamを、Stringで構成されたStreamに置き換えています。

map
list.stream()
.map(x -> x.getKey() + x.getValue() + x.getDescription())
.forEach(x -> System.out.println(x));
実行結果
Bぶたまん豚肉が入っています
Aあんまんあんこが入っています
Cちゅうかまんあんまんぶたまん等の総称です

flatMap

Streamの中にStreamを作れるようです。
Streamで表を作るようなイメージ?
ここでは、1行を構成しているBeanの各項目をバラしてもう1つのStreamを作っています。

flatMap
list.stream()
.flatMap(x -> Stream.of(Arrays.asList(x.getKey(),x.getValue(),x.getDescription())))
.forEach(x -> x.forEach(y -> System.out.println(y)));
実行結果
B
ぶたまん
豚肉が入っています
A
あんまん
あんこが入っています
C
ちゅうかまん
あんまんぶたまん等の総称です

distinct

重複要素を排除。
このテストをするにあたって、前述のテストデータを水増ししました。

テストデータ(aが重複)
    private static List<TestBean> editTestData() {
        List<TestBean> list = new ArrayList<>();
        TestBean a = new TestBean("A", "あんまん", "あんこが入っています");
        TestBean b = new TestBean("B", "ぶたまん", "豚肉が入っています");
        TestBean c = new TestBean("C", "ちゅうかまん", "あんまんぶたまん等の総称です");
        list.add(b);
        list.add(a);
        list.add(a);
        list.add(a);
        list.add(c);
        return list;
    }
distinctなし
list.stream()
.forEach(x -> System.out.println(x.getDescription()));
実行結果(distinctなし)
豚肉が入っています
あんこが入っています
あんこが入っています
あんこが入っています
あんまんぶたまん等の総称です
distinctあり
list.stream()
.distinct()
.forEach(x -> System.out.println(x.getDescription()));
実行結果(distinctあり)
豚肉が入っています
あんこが入っています
あんまんぶたまん等の総称です

sorted

その名の通りソートします。
この例ではkeyと名付けた項目(ABC)の順でソート。

sorted
list.stream()
.sorted((o1, o2) -> o1.getKey().compareTo(o2.getKey()))
.forEach(x -> System.out.println(x.getKey() + x.getValue() + x.getDescription()));
実行結果
Aあんまんあんこが入っています
Bぶたまん豚肉が入っています
Cちゅうかまんあんまんぶたまん等の総称です

peek

これは特にストリームの中身を操作するものではなく、
デバッグのためにあるメソッドのようです。
peek = ちらっとのぞく(直訳)

peek
list.stream()
.peek(x -> System.out.println(x))
.forEach(x -> System.out.println("  forEachで出力しています"));
実行結果
key:B value:ぶたまん description:豚肉が入っています
  forEachで出力しています
key:A value:あんまん description:あんこが入っています
  forEachで出力しています
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です
  forEachで出力しています

最初にpeekが全部行われてから、最後にforEachが行われるのかと思っていましたが、1要素ごとに中間処理→終端処理されているみたいですね。

limit

Streamの数を指定数までに切り詰めます。

limit
list.stream()
.limit(2)
.forEach(x -> System.out.println(x));
実行結果
key:B value:ぶたまん description:豚肉が入っています
key:A value:あんまん description:あんこが入っています

skip

limitとは逆に、Streamの頭を読み飛ばします。

skip
list.stream()
.skip(1)
.forEach(x -> System.out.println(x));
実行結果
key:A value:あんまん description:あんこが入っています
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です

終端操作

forEach

何気なくここまで毎回使っていました。
Streamの全要素に対して処理をします。
…が、順序性が保証されないようです。並列ストリームの場合のみ?

forEachOrdered

forEachと同じなのですが、こちらを使うと順序性が保証されるようです。

toArray

ストリームを単純に配列にします。

toArray
Object[] testBeans = list.stream().toArray();
System.out.println(testBeans[0].toString());
System.out.println(testBeans[1].toString());
実行結果
key:B value:ぶたまん description:豚肉が入っています
key:A value:あんまん description:あんこが入っています

reduce

reduce = 削減、減らす(直訳)
Streamの内容を集約するときに便利なようです。
この例ではvalueと名付けた項目をくっつけています。
Streamからは1つのOptionalだけが返ります。
その他、初期値をセットできるreduceもあります。シグネチャ違い。

reduce
Optional<TestBean> opt = list.stream()
.reduce((t, u) -> {
  t.setValue(t.getValue() + u.getValue());
  return t;
});
System.out.println(opt.orElse(new TestBean("x", "x", "x")));
実行結果
key:B value:ぶたまんあんまんちゅうかまん description:豚肉が入っています

collect

Optionalのような単一要素ではなく、Collectionとして返したいときに使うっぽいです。
Collection → Stream化して編集 → Collectionとして出力
かなりよく使うのではないかと思います。

collectの引数となっている各種インターフェースを自力で真面目に実装しようとすると、抽象メソッドを5つほど書かないといけなくて大変なので、Collectorsクラスに用意されているstaticメソッドを使うのが楽ちんです。

collect
List<TestBean> newlist = list.stream()
  .filter(x -> x.getKey().equals("C"))
  .collect(Collectors.toList());
System.out.println(newlist.get(0).toString());
実行結果
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です

min

その名の通り、最小のものをOptionalで返します。
何が最小かっていうのは、sortのときと同じで、Comparatorを実装して定義します。
この例では、keyという項目を指定。

min
Optional<TestBean> op = list.stream()
  .min((x, y) -> x.getKey().compareTo(y.getKey()));
System.out.println(op.orElse(new TestBean("x", "x", "x")).toString());
実行結果
key:A value:あんまん description:あんこが入っています

max

minの反対です!

max
Optional<TestBean> op = list.stream()
  .max((x, y) -> x.getKey().compareTo(y.getKey()));
System.out.println(op.orElse(new TestBean("x", "x", "x")).toString());
実行結果
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です

count

その名の通り、Stream要素の数を返します。

count
System.out.println(list.stream().count());
実行結果
3

anyMatch

読んだまんまですが、Stream内のいずれかが条件に該当したらtrueを返すものです。
この例では、keyという項目が"A"のものを探しています。

anyMatch
boolean matched = list.stream().anyMatch(x -> x.getKey().equals("A"));
if (matched) {
  System.out.println("あんまんがいたよ!");
}
実行結果
あんまんがいたよ!

allMatch

これも読んだまんまで、Stream内の全ての要素が指定条件に該当したらtrueを返します。

allMatch
boolean matched = list.stream().allMatch(x -> !x.getKey().equals("Z"));
if (matched) {
  System.out.println("みんなZではないね!");
}
実行結果
みんなZではないね!

noneMatch

Stream内に該当するものがなかったらtrueを返します。

noneMatch
boolean noMatched = list.stream().noneMatch(x -> x.getKey().equals("Z"));
if (noMatched) {
  System.out.println("Z戦士はいなかったよ");
}
実行結果
Z戦士はいなかったよ

findFirst

その名の通り、最初の1件を返します!

findFirst
Optional<TestBean> op = list.stream().findFirst();
System.out.println(op.orElse(new TestBean("x", "x", "x")).toString());
実行結果
key:B value:ぶたまん description:豚肉が入っています

findAny

その名のを通り、なんかを返します。
なんかってなんだよ…って話ですが、
ほんとに何が返ってくるのかわかりません。引数指定できないし。
順次ストリームで試したら先頭要素が返ってきましたが、保証はされないそうです。
(2018/12/08 コメントいただき修正しました)

findAny
Optional<TestBean> op = list.stream().findAny();
System.out.println(op.orElse(new TestBean("x", "x", "x")).toString());
実行結果
key:B value:ぶたまん description:豚肉が入っています

メソッドを試す以外の実験

Streamを操作しても元のソースが変わっていないことの確認

Stream内のbeanのvalueという項目を固定文字列で上書きしました。
たしかにソース(List)は変わっていませんでしたよ。

System.out.println("【変更前】");
for (TestBean bean : list) {
  System.out.println(bean);
}

list.stream().map(x -> {
  x.setValue("変えちゃいました!");
  return x;
});

System.out.println("【変更後】");
for (TestBean bean : list) {
  System.out.println(bean);
}
実行結果
【変更前】
key:B value:ぶたまん description:豚肉が入っています
key:A value:あんまん description:あんこが入っています
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です
【変更後】
key:B value:ぶたまん description:豚肉が入っています
key:A value:あんまん description:あんこが入っています
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です

速度差を測定

1億5千万件のListを作って
その中に0〜99の乱数を格納し
99が格納されている要素だけ抜き出して別Listを作る
という無駄な処理をして計測しました。

結果、Listとstreamは大差なし、
paralellStreamは明らかに速かったです。

    final int loopsize = 150000000;
    final int randomrange = 100;
    final int findnum = 99;

    // create many data
    System.out.println("テストデータ生成中...");
    List<Integer> list = IntStream.range(0, loopsize)
        .mapToObj(x -> new Random().nextInt(randomrange))
        .collect(Collectors.toList());

    // byList
    LocalDateTime starttime = start("--- byList ---");
    List<Integer> newlist = new ArrayList<>();
    for (Integer i : list) {
      if (i.intValue() == findnum) {
        newlist.add(i);
      }
    }
    end(starttime, findnum, newlist);

    // byStream
    starttime = start("--- byStream ---");
    List<Integer> streamlist = list.stream()
        .filter(x -> x.intValue() == findnum).collect(Collectors.toList());
    end(starttime, findnum, streamlist);

    // byParalellStream
    starttime = start("--- byParalellStream ---");
    List<Integer> paralellStreamlist = list.parallelStream()
        .filter(x -> x.intValue() == findnum).collect(Collectors.toList());
    end(starttime, findnum, paralellStreamlist);
  }

  private static LocalDateTime start(String msg) {
    System.out.println("");
    System.out.println(msg);
    LocalDateTime ldt = LocalDateTime.now();
    System.out.println("start: " + ldt);
    return ldt;
  }

  private static void end(LocalDateTime starttime, int findnum,
      List<Integer> newlist) {
    LocalDateTime elapsed = LocalDateTime.now();
    System.out.println("end  : " + elapsed);
    elapsed = elapsed.minusHours(starttime.getHour());
    elapsed = elapsed.minusMinutes(starttime.getMinute());
    elapsed = elapsed.minusSeconds(starttime.getSecond());
    elapsed = elapsed.minusNanos(starttime.getNano());
    System.out
        .println("elapsed: " + elapsed.format(DateTimeFormatter.ISO_TIME));
    System.out.println(findnum + "は" + newlist.size() + "個ありました!");
  }
実行結果
テストデータ生成中...

--- byList ---
start: 2018-12-08T15:35:51.584
end  : 2018-12-08T15:35:52.039
elapsed: 00:00:00.455
99は1500072個ありました!

--- byStream ---
start: 2018-12-08T15:35:52.052
end  : 2018-12-08T15:35:52.450
elapsed: 00:00:00.398
99は1500072個ありました!

--- byParalellStream ---
start: 2018-12-08T15:35:52.450
end  : 2018-12-08T15:35:52.672
elapsed: 00:00:00.222
99は1500072個ありました!

forEach、findAnyの順序性が保証されないことを確認

parallelStreamではたしかに保証されませんでした。
順次Streamでは元のソース(List)の順でアクセスされましたよ。

    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

    System.out.println("--- byList ---");
    for (int i : list) {
      System.out.println(i);
    }

    System.out.println("--- byStream ---");
    list.stream().forEach(i -> System.out.println(i));

    System.out.println("--- byParallelStream ---");
    list.parallelStream().forEach(i -> System.out.println(i));

    System.out
        .println("findany by stream:" + list.stream().findAny().orElse(null));

    System.out.println("findany by parallelStream:"
        + list.parallelStream().findAny().orElse(null));
実行結果
--- byList ---
1
2
3
4
5
6
7
8
9
10
--- byStream ---
1
2
3
4
5
6
7
8
9
10
--- byParallelStream ---
7
9
6
3
5
2
10
1
4
8
findany by stream:1
findany by parallelStream:7

戸惑った点

Streamインターフェースの各メソッドの引数

データではなく関数型インターフェース(振舞い)を引数として渡している。
これが関数型プログラミングというやつか。
手続き型の書き方しかしてこなかったので、すっと書けなかった。

すぐ忘れてしまって一発で書けなくなるので、ラムダ式を使わずに、new インターフェース(){...}して無名クラスを作って、抽象メソッドを実装したうえで、ラムダ式に置き換えています。
お恥ずかしながら...。
まぁそのうち慣れるでしょう。

参考にさせていただいた記事

著者の皆様、ありがとうございました!

StreamAPIの基本
https://qiita.com/Takmiy/items/f1d44dfde0d3a906d321

Java8のFunction, Consumer, Supplier, Predicateの調査
https://qiita.com/subaru44k/items/c55d9b9fc419f0d09c64

関数型プログラミングって何、ラムダってなんだよ
https://qiita.com/lrf141/items/98ffbeaee42d30cca4dc

StreamのmapとflatMapの違い
https://qiita.com/KevinFQ/items/97137efb2159009b60e1

Qiita外。StreamAPIの目的から実験結果まで書いてある。
http://d.hatena.ne.jp/nowokay/20130506