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つのソース(Listとか)からStreamを生成
- 0〜n個の中間操作
- 1つの終端操作
ポイント(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
その名の通りフィルタリングしてくれました!
list.stream()
.filter(x -> x.getValue().equals("あんまん"))
.forEach(x -> System.out.println(x.getDescription()));
あんこが入っています
map
Streamを基に別のStreamを作るもの、と解釈しました。
この例では、TestBean型で構成されたStreamを、Stringで構成されたStreamに置き換えています。
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を作っています。
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
重複要素を排除。
このテストをするにあたって、前述のテストデータを水増ししました。
テストデータ
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あり
list.stream()
.distinct()
.forEach(x -> System.out.println(x.getDescription()));
豚肉が入っています
あんこが入っています
あんまんぶたまん等の総称です
sorted
その名の通りソートします。
この例ではkeyと名付けた項目(ABC)の順でソート。
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 = ちらっとのぞく(直訳)
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の数を指定数までに切り詰めます。
list.stream()
.limit(2)
.forEach(x -> System.out.println(x));
key:B value:ぶたまん description:豚肉が入っています
key:A value:あんまん description:あんこが入っています
skip
limitとは逆に、Streamの頭を読み飛ばします。
list.stream()
.skip(1)
.forEach(x -> System.out.println(x));
key:A value:あんまん description:あんこが入っています
key:C value:ちゅうかまん description:あんまんぶたまん等の総称です
終端操作
forEach
何気なくここまで毎回使っていました。
Streamの全要素に対して処理をします。
…が、順序性が保証されないようです。並列ストリームの場合のみ?
forEachOrdered
forEachと同じなのですが、こちらを使うと順序性が保証されるようです。
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もあります。シグネチャ違い。
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メソッドを使うのが楽ちんです。
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という項目を指定。
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の反対です!
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要素の数を返します。
System.out.println(list.stream().count());
3
anyMatch
読んだまんまですが、Stream内のいずれかが条件に該当したらtrueを返すものです。
この例では、keyという項目が"A"のものを探しています。
boolean matched = list.stream().anyMatch(x -> x.getKey().equals("A"));
if (matched) {
System.out.println("あんまんがいたよ!");
}
あんまんがいたよ!
allMatch
これも読んだまんまで、Stream内の全ての要素が指定条件に該当したらtrueを返します。
boolean matched = list.stream().allMatch(x -> !x.getKey().equals("Z"));
if (matched) {
System.out.println("みんなZではないね!");
}
みんなZではないね!
noneMatch
Stream内に該当するものがなかったらtrueを返します。
boolean noMatched = list.stream().noneMatch(x -> x.getKey().equals("Z"));
if (noMatched) {
System.out.println("Z戦士はいなかったよ");
}
Z戦士はいなかったよ
findFirst
その名の通り、最初の1件を返します!
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 コメントいただき修正しました)
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