StreamAPIについてまとめてみる
はじめに
開いていただきありがとうございます。
会社の後輩に「StreamAPIが分からないので教えてください!」と言われたので、「どうせ教える用に資料作成するならzennで公開するか」と思い作成しました。本記事ではStreamAPIについて説明しております。
StreamAPIとは?
JDK8以降で導入され、コレクション(List,Map)や配列に対して何らかの処理(集計や変換)を行うことができます。
Streamはコレクションフレームワーク(List・MapやCollectionsクラス等)やjava.io.InputStream
やjava.io.OutputStreamやPrintStream
等とは全く別のものです。
どう使うの?
基本的な使い方は下記の通りになります。
-
Stream
を取得します。-
List
やMap
などのコレクションからStream
を生成します。 - 基本的には
List
やMap
などのコレクション
-
- 中間操作を実施します。
- 変換や集計などのデータ加工を行います。
- 中間操作は複数のメソッドをチェーンすることができる。
- 2で処理したものに対して、終端操作を実施します。
-
Stream
の全データを一括で別のデータに変換します。
-
文字だけで書いてもイメージが沸かないと思うので、サンプルを載せてみました。
// Listを生成。
List<String> list = Arrays.asList("a", "bb", "ccc");
List<String> filterList =
// ListをStreamに変換(Streamの生成)
list.stream()
// 中間操作。例は2文字のものをフィルターする。
.filter(s -> s.length() == 2)
// 終端操作。StreamからListに変換する。
.collect(Collectors.toList());
上記の処理を実行すると「bb」のみを抽出することができます。
なぜStreamを使うのか
先ほどのサンプルコードをStreamを使わずに書く場合はどうなるでしょうか?
Streamを使わない例です。
// Listの生成
List<String> list = Arrays.asList("a", "bb", "ccc");
// フィルター結果格納用
List<String> filterList = new ArrayList<>();
// Listの各要素をイテレート
// 通常のfor文
for (int i = 0; i < list.size(); i++) {
// 長さが2文字の場合のみリストに追加する。
if (list.get(i).length() == 2) {
filterList.add(list.get(i);
}
}
// フィルター結果格納用
List<String> filterList2 = new ArrayList<>();
// 拡張for文バージョン
for (String s : list) {
// 長さが2文字の場合のみリストに追加する。
if (s.length() == 2) {
filterList2.add(s);
}
}
こちらも同じく「bb」のみを抽出する処理となります。
これはこれで正解なのですが、Streamを使って書く方が良いとされています。その理由として、
- コードの行数が少なくなる
- より直感的に表現することができる
- 今回の例だと「値を条件でフィルターしてリストに変換する。」といった感じでコード読むことができる。(慣れの問題もありますが。。。)
- ダミーの初期値を用意して、それぞれの文字をイテレートして追加するといった複数の段階に分けてコードを書く必要がなくなる。
- 並列処理が行いやすい
ことが主に挙げられます。
このようにStreamAPIを使用することで、従来の書き方とは異なりシンプルでわかりやすいコードを書くことができます。
中間操作
中間操作とは
中間操作とはStream
内の要素に対して操作を行います。中間操作はメソッドチェーンで連結していくことができます。
中間操作は即時実行されますが、渡されたラムダ式やメソッド参照はすぐに評価されるわけではありません。
これらのメソッドが行う処理はあとの実行時のためにキャッシュされ、呼ばれるまでは何も実作業を行いません。
キャッシュされた処理は終端処理が呼び出される際に実行されます。
中間操作のメソッド
中間操作のメソッドには以下の種類があります。
filter
条件を満たした値だけを抽出するメソッドです。
Predicate<T>
(Streamの要素Tを引数に取り、booleanを返す関数)を引数に取るメソッドです。
ある要素のコンテクストで実行された関数がtrue
を返す場合、その要素は結果のコレクションに格納されます。
false
を返す場合その要素は無視され、格納されません。関数がtrue
を返した要素だけを格納したStreamを返却します。
すでに「StreamAPIとは?」でfliter
メソッドを使用したサンプルを再掲します。
// Listを生成。
List<String> list = Arrays.asList("a", "bb", "ccc");
// 文字列の長さが「2」のものを抽出する。
List<String> filterList =
list.stream()
.filter(s -> s.length() == 2)
.collect(Collectors.toList());
// 実行結果
// bb
map,mapToInt,mapToLong,mapToDouble,mapToObj
保持している値を変換します。
Function<T,R>
(Streamの要素Tを引数に取り、R型の戻り値を返す関数。TとRは同じ型でも良い)を引数に取るメソッドです。
入力されたコレクションを新たなコレクションに変換する際に非常に便利です。
このメソッドでは入力されたコレクションと新たに生成されたコレクションが常に同数の要素を持つことを保証します。
しかし、型は入力されたコレクションと新たに生成されたコレクションで必ずしも一致するとは限りません。
mapにはint型に変換するmapToInt
,long型に変換するmapToLong
,double型に変換するmapToDouble
,オブジェクトに変換するmapToObj
が存在します。
// Listを生成。
List<String> list = Arrays.asList("a", "bb", "ccc");
// リストの文字列を大文字に変換する。
list.stream()
.map(s -> s.toUpperCase())
.forEach(s -> System.out.print(s + " "));
// 実行結果
// A BB CCC
flatMap,flatMapToInt,flatMapToLong,flatMapToDouble,flatMapToDouble
複数の値(0個も可)に変換します。
mapメソッドが行うようにコレクションの要素をマップしますが、引数にFunction<T, Stream<R>>
という1つの要素TからStream<R>型を生成する関数を取ります。
flatMapにもmapと同様にint型に変換するflatMapToInt
,long型に変換するflatMapToLong
,double型に変換するflatMapToDouble
が存在します。
final List<String> player =
Arrays.asList("Imai", "Takahasi", "Masuda", "Matsumoto", "Matsuzaka");
// 文字列(String)から[文字列(String), 文字列長(Integer)]へ1:Nの変換
Stream<Object> stream = player.stream().flatMap(p -> Stream.of(p, p.length()));
System.out.println(stream.collect(Collectors.toList()));
// 実行結果
// [Imai, 4, Takahasi, 8, Masuda, 6, Matsumoto, 9, Matsuzaka, 9]
sorted
文字通りソートします。このメソッドには2種類あります。
- 保持されているデータが
java.lang.Comparable
を実装しているパターン - 関数型インターフェース
Comparator
を渡してソート順を定義するパターン
1はComparable
を実装しているため、引数なしでソート可能です。
2はComparator
は関数型インタフェースなので、互換性のある関数(ラムダ式)を渡します。
final List<Integer> numList = Arrays.asList(5, 9, 2, 1, 0);
// 1.のやり方でソートする。
numList.stream().sorted().forEach(System.out::println);
// 実行結果
// 0
// 1
// 2
// 5
// 9
limit
指定された個数までに限定します。最大個数を渡します。
final List<String> player =
Arrays.asList("Imai", "Takahasi", "Masuda", "Matsumoto", "Matsuzaka");
// 5文字より大きいものを抽出し、2つまでに限定する。
player.stream()
.filter(s -> s.length() > 5)
.limit(2)
.forEach(System.out::println);
// 実行結果
// Takahasi
// Masuda
skip
指定された個数をスキップします。スキップする個数を渡します。
final List<String> player =
Arrays.asList("Imai", "Takahasi", "Masuda", "Matsumoto", "Matsuzaka");
// 2つ分スキップする。
player.stream()
.skip(2).forEach(System.out::println);
// 実行結果
// Masuda
// Matsumoto
// Matsuzaka
distinct
重複した値を削除します。
final List<String> distinctList
= Arrays.asList("a", "bb", "ccc", "a");
// 重複を取り除く。今回は「a」。
distinctList.stream().distinct().forEach(System.out::println);
// 実行結果
// a
// bb
// ccc
peek
デバック用途で、どんな値が入ってるか確認したい時などに使用します。
終端操作
終端操作とは・・・
終端操作とはStream
の中の要素を実際に処理する工程であり、fliter
等の中間操作は終端操作をトリガーにして初めて実行される。
中間操作を適用した要素に対して、終端操作で処理し、最終的にStream
から別のデータへの変換を行います。
終端操作の役割として以下の2つが挙げられます。
-
Stream
から他のデータへ変換 -
検索
や集計
などの操作を行う
終端操作のメソッド
終端操作のメソッドには以下の種類があります。
- 変換
collect
toArray
- 検索
-
findFirst
,findAny
-
allMatch
,anyMatch
,noneMatch
-
- 集約・集計
-
average
,sum
,count
,max
,min
-
average
,sum
はプリミティブ型でのみ提供されています。
-
summaryStatistics
reduce
-
- 出力
forEach
forEachOrdered
iterator
各メソッドについて
サンプルコードで使用する不変リストは下記の通りです。
List<String> player =
Arrays.asList("Imai", "Takahasi", "Masuda", "Matsumoto", "Matsuzaka");
collect
あるコレクションを可変コレクションなど他のデータ型へ変換する際に便利な集約処理を行います。
Collectorsクラスのユーティリティメソッドと組み合わせるととても便利です。
Collectorsクラスは以下のページを参照してもらえればと思います。
Streamの要素をArrayListに変換する際に使用することが多いです。
// 文字列を大文字に変換し、「/」で連結します。
System.out.println(player.stream()
.map(String::toUpperCase)
.collect(Collectors.joining("/ ")));
// 実行結果
// IMAI/ TAKAHASI/ MASUDA/ MATSUMOTO/ MATSUZAKA
toAarray
配列に変換します。配列インスタンスを生成する関数を渡します。
// 文字列を大文字に変換し、出力します。(こんな周りくどい書き方普通しない。。。)
String[] array =
player.stream()
.map(String::toUpperCase)
.toArray(String[]::new);
System.out.println(Arrays.toString(array));
// 実行結果
// [IMAI, TAKAHASI, MASUDA, MATSUMOTO, MATSUZAKA]
count
要素数を返却します。
// 先頭の文字が「M」のものを抽出し、その数をカウントします。
System.out.println(player.stream().filter(n -> n.startsWith("M")).count());
// 実行結果
// 3
max,min
最大値(最小値)を返却します。
// 文字列の長さが最小(最大)の選手を出力します。
System.out.println(player.stream().min((s, t) -> s.length() - t.length()));
System.out.println(player.stream().max((s, t) -> s.length() - t.length()));
// 実行結果
// Optional[Imai]
// Optional[Matsumoto]
average
平均値を返却します。
// 文字列の長さの平均値を算出します。
System.out.println(player.stream().mapToDouble(p -> p.length()).average());
// 実行結果
// OptionalDouble[7.2]
sum
合計値を返却します。
// 文字列の長さの合計を算出します。
System.out.println(player.stream().mapToInt(p -> p.length()).sum());
// 実行結果
// 36
reduce
2つの要素を比較し、「その結果を次の要素と比較のために渡す」という処理をコレクション内の最後の要素まで繰り返します。
リストが空である可能性があるので、メソッドの戻り値はOptional
になります。
// リストの要素を「/」で結合して返却します。
Optional<String> foundPlayer =
player.stream()
.reduce((String joined, String element) -> {
return joined + '/' + element;
});
System.out.println(foundPlayer);
// 実行結果
// Optional[Imai/Takahasi/Masuda/Matsumoto/Matsuzaka]
findFirst,findAny
先頭の値(ストリーム内のいずれかの値)を返却します。空のStreamの場合はOptional.empty
を返却します。
// 先頭の文字列が「M」で最初の選手を表示します。
Optional<String> foundPlayer =
player.stream()
.filter(n -> n.startsWith("M"))
.findFirst();
System.out.println(foundPlayer);
// 実行結果
// Optional[Masuda]
allMatch,anyMatch,noneMatch
条件判定の関数Predicate
を条件にStream
結果の要素を検索し、マッチする要素の存在状態を判定します。
allMatch
は全ての値が条件に合致したらtrueを返却します。
anyMatch
は1つでも条件に合致する値があればtrueを返却します。
noneMatch
は条件に合致する値が1つもなかったらtrueを返却します。
// 先頭の文字列が「T」の選手が一人でもいるかをチェックします。
System.out.println(player.stream().anyMatch(p -> p.startsWith("T")));
// 実行結果
// true
// 全ての選手の先頭の文字列が「M」の選手がどうかをチェックします。
System.out.println(player.stream().allMatch(p -> p.startsWith("M")));
// 実行結果
// false
forEach,forEachOrdered
コレクションの要素をイテレートします。Consumer型
を引数に取ります。
forEachメソッドを開始するとイテレーションから抜けることができません。(この制限を処理する機能は存在します。)
そのため、この書き方はコレクションの各要素を処理したい場合に有用です。
forEachOrdered
は値を1つずつ順番に処置するメソッドです。
// 5文字より大きいものを抽出し、2つまでに限定したものを出力します。
player.stream().filter(s -> s.length() > 5)
.limit(2)
.forEach(System.out::println);
// 実行結果
// Takahasi
// Masuda
最後に・・・
最後まで読んでいただきありがとうございました。
知らなかったメソッドもあったので改めて勉強になりました。
StreamAPIで必ず使用する「ラムダ式」や「メソッド参照」についても以下の記事にまとめているので、参照いただければと思います。
Discussion