Java8 で導入された Stream の使い方をはじめて使ってみる人向けに体系的に説明してみた
Stream とは
Java8 より、新規に追加された stream パッケージのこと。Collection に対する処理(集計、変換)を宣言的に記述することをサポートする。 パッケージは、java.util.stream となっており、Collection のインターフェースとは別パッケージとなっている。 日本語APIはこちら
オブジェクトに対する Stream と、プリミティブに対する Stream
対象とする Collection の種別により、異なる Stream インターフェースが用意されている。
オブジェクトに対する Stream
@Test public void objectStream_sum() { List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10); int sum = list.stream().mapToInt(Integer::intValue).sum(); assertThat(sum).isEqualTo(55); }
プリミティブに対する Stream
interface IntStream, interface DoubleStream, interface LongStream
@Test public void intStream_sum() { // 1 - 100 までの総和 int sum = IntStream.rangeClosed(1, 100).sum(); assertThat(sum).isEqualTo(5050); }
@Test public void doubleStream_sum() { // double 値の総和 double sum = DoubleStream.of(2.5d, 3.7d, 4.98d).sum(); assertThat(sum).isEqualTo(11.18); }
Stream メソッドの種類
Stream のメソッドは、大きく中間処理と、末端処理の2種類に分けられる。中間処理は、処理の終わりに Stream 自体を返却するメソッド。それ以外のメソッドを終端処理と呼ぶ。「オブジェクトに対する Stream」の例にある、mapToInt() が中間処理、sum() が終端処理となる。
中間処理
中間処理は、filter()、map()など、処理の終わりに Stream 自体を返却するメソッド。Stream 内に保持されている値に対しての操作は、中間処理中は実施されない。
終端処理
終端処理は、count()、anyMatch() など、処理の終わりに、処理結果を返却する。 終端処理の呼び出しの際に、実際の処理が実施される。そのため、大量データの処理であっても実行時間がかかるのは、終端処理となり、中間処理はそれ程時間はかからない。
Stream インターフェースのメソッド分類
Stream
static | 中間処理 | 終端処理 | その他 |
---|---|---|---|
builder | allMatch | collect | forEach |
concat | anyMatch | count | forEachOrdered |
empty | distinct | findAny | |
generate | filter | findFirst | |
iterate | flatMap | max | |
of | flatMapToDouble | min | |
flatMapToInt | noneMatch | ||
flatMapToLong | reduce | ||
limit | toArray | ||
map | |||
mapToDouble | |||
mapToInt | |||
mapToLong | |||
peek | |||
skip | |||
sorted |
Stream の使い方
ここまで、Stream とは何かを体系から説明してきた。Stream とは、Collection に対して変換、集計を実施する。また、オブジェクトとプリミティブでは、異なる Stream を利用する。Stream メソッドには、中間処理、終端処理があるんだと理解できていれば良いと思う。 ここでは、それらを踏まえて、実際に Stream の使い方を理解する。
Stream の使い方は至ってシンプルだ。下記の3Stepのみである。上から下に流れて行き、逆流はできない。
- Stream を作る
- 中間処理を行う
- 終端処理を行う
@Test public void stream_step() { List<String> nuumbers = Arrays.asList("1","2","3","4","5","6","7","8","9","10"); int sum = nuumbers.stream() // 1. numbers collection から、 Stream<String> を作る .mapToInt(Integer::parseInt) // 2. 中間処理で、Stringから、int に変換 .sum(); // 3. 終端処理で、総和を求める assertThat(sum).isEqualTo(55); }
中間処理は、Stream 自身を返却するため、何度も適用できる。先の例に、fileter の中間処理を追加してみる。
@Test public void stream_intemediate_operation_any() { List<String> nuumbers = Arrays.asList("1","2","3","4","5","6","7","8","9","10"); int sum = nuumbers.stream() // 1. numbers collection から、 Stream<String> を作る .mapToInt(Integer::parseInt) // 2-1. 中間処理で、Stringから、int に変換 .filter(v -> v % 2 == 0) // 2-2. 中間処理で、偶数のみ抽出 .sum(); // 3. 終端処理で、総和を求める assertThat(sum).isEqualTo(30); }
よく使う中間処理と終端処理
よく使う、中間処理と終端処理の使い方を記載する。
Collection の中身を利用して、何かの値を作成し、リストに変換
先ずは、Collection の中身を利用して、何かの値を作成し、リストに変更するパターン。殆どのループがこれなのではないだろうか。
例では、中間処理の map で、 key と value を結合し、表示用の文字列を返却している。終端処理の collect で、List にまとめて出来上がり。
@Value private static class Hoge { private String key; private String value; } @Test public void stream_map() { List<Hoge> hoges = new ArrayList<>(); Hoge hoge1 = new Hoge("hoge1", "HOGE1"); hoges.add(hoge1); Hoge hoge2 = new Hoge("hoge2", "HOGE2"); hoges.add(hoge2); Hoge hoge3 = new Hoge("hoge3", "HOGE3"); hoges.add(hoge3); List<String> dispNames = hoges.stream() .map(v -> new Formatter().format("%s: %s", v.getKey(), v.getValue()).toString()) .collect(Collectors.toList()); assertThat(dispNames.get(0)).isEqualTo("hoge1: HOGE1"); }
Collection の中身をフィルタリングし、オブジェクトを生成し、リストに変換
上の「Collection の中身を利用して、何かの値を作成し、リストに変換」に似ているが、オブジェクトの生成に注目して欲しい(map(new)のところ)。むしろ、fileter はおまけ。
Stream とは直接関係無いが、オブジェクトの生成を外に出すことにより、非常に見通しの良いコードになっている。別に For ループでもそう書くって? しかし、経験上、ループ中の new からの Setter 乱舞は非常に良く見かける。
ラムダ式の恩恵なのだが、Stream 中にそのような暴挙をみることがないのは、嬉しい副産物だと思う。
@Value private static class Hoge { private String key; private String value; } @Test public void stream_newInstance() { Map<String, String> dictionary = new HashMap<>(); dictionary.put("hoge1", "HOGE1"); dictionary.put("hoge2", "HOGE2"); dictionary.put("hoge3", "HOGE3"); List<Hoge> hoges = dictionary.entrySet().stream() .filter(v->v.getKey().equals("hoge2")) .map(v -> new Hoge(v.getKey(), v.getValue())) .collect(Collectors.toList()); assertThat(hoges.get(0).getValue()).isEqualTo("HOGE2"); }
まとめ
Stream とは、Collection に対する処理を宣言的に記述できるもの。
中間処理と終端処理があり、逆流はできない。
ある値の Collection をある値の Collection に変換する Dxo のような処理には、非常にパワフルに働く。もちろん、それ以外でもだけど。
Stream は、ラムダ式(v -> ってやつ)や、メソッド参照(:: ってやつ)のお陰で取っ付き難い印象があるが、殆どの Collection 処理は、もうコレで良いのでは無いかと思うほど、力強い。
短く簡潔な宣言的なコードには、バグが入り込む余地は少なくなり、コードを読む時間も短縮できる。良いこと尽くめなので、毛嫌いせずに、是非、導入して頂きたい。