前回、Java8のラムダ式について記事(【初心者向け】今更聞けない?Java8のラムダ式について知ろう!
を書きました金巻です。
今回も引き続きJava8の新機能についてご紹介したいと思います。

Java 8では新たに導入される機能で特に注目を浴びている「Stream API」というものがあります。
こちらについて今回はご紹介いたします。

Stream APIについて

Stream APIはJava8で導入された機能の中で特に注目されている機能で、配列やCollectionなどの集合体を扱う為のもので、値の集計やデータを使った処理などが出来る便利なAPIです。

Streamの種類としては幾つかのインタフェースが用意されています。
よく使われるものとしては以下のものがあります。

インタフェース 概要
Stream<t> 要素がTのインスタンスのStream
IntStream 要素がintの値のStream
LongStream 要素がlongの値のStream
DoubleStream 要素がdoubleの値のStream

プリミティブ型にはそれに応じたStreamインタフェースが用意されています。
JavaはStringをプリミティブ型ではないとしているのでStringStreamはありません。

昔はAとBの処理を同時に実行するには2台のPCにそれぞれの処理を振り分けて実行することで
時間の短縮をしていました。これは分散処理と言われています。
これは1つのPCに1つのCPUしかなかったからでした。

しかし、今は1つのPCに複数のCPUがあり、AとBの処理を実行する事が出来ます。
これが並列処理というものです。
現在は並列と分散を組み合わせた並列分散処理という形が広く使われています。
そこでJava8にも並列処理を可能とにするStream APIがついに追加されたのでしょう。

StreamAPIの使い方は中間操作と終端操作!

Stream APIは基本的に『中間操作』、『終端操作』という使い方になります。
Streamが持っている要素から0個以上の中間処理を行い変換したりして、最終的な処理を行うメソッドで結果を取得するということです。
それまでのfor文で記述していた部分はStream APIに置き変えることが可能になります。
ただ、注意しなければならない点はあります。
例えばListは順番を保障していますが、Streamにて並列で実施する場合にはその順番が関係なくなることなどです。

具体的な例は以下のとおりです。

List<String> lists = Arrays.asList("tokyo", "ibaraki", "saitama");
Stream<String> stream = lists.stream();   // 直列ストリーム
Stream<String> pstream = lists.parallelStream();   // 並列ストリーム
stream.forEach(System.out::println);
System.out.println("============");
pstream.forEach(System.out::println);

結果は以下のようになります。

tokyo
ibaraki
saitama
============
ibaraki
saitama
tokyo

pstreamの方ではListの順番に関係なく出力されています。

さてそれでは続きまして『中間操作』、『終端操作』について順に具体例を出しながら説明したいと思います。

中間操作について

操作をしていく前にまず中間処理を行うメソッドをまず知りましょう。
主なものに次のメソッドがあります。

メソッド 概要
filter(Predicate predicate) Predicateで定義したbooleanの判定がtrueの要素のみに絞ったStreamを返す
limit(long maxSize) 要素の最初からmaxSizeまでの要素のStreamを返す
distinct() 要素同士を比較し重複するものを除いたStreamを返す

メソッド名で大体何をやってくれるか想像つきますが、念のため例をあげて確認してみましょう。

ソース

Integer[] array = {1,2,3,4,5,6,7,1,2,3,4,5,6};
System.out.println("filterメソッド");
Stream<Integer> stream1 = Arrays.stream(array);
stream1.parallel();
// 要素を3の倍数でフィルターします
Stream<Integer> filterStream = stream1.filter(value -> value%3 == 0);
filterStream.parallel();
filterStream.forEach(value -> System.out.println("filterStream: " + value));
System.out.println("limitメソッド");
Stream<Integer> stream2 = Arrays.stream(array);
stream2.parallel();
// 要素を最初から3要素までに絞ります
Stream<Integer> limitStream = stream2.limit(3);
limitStream.forEach(value -> System.out.println("limitStream: " + value));
System.out.println("distinctメソッド");
Stream<Integer> stream3 = Arrays.stream(array);
stream3.parallel();
// 要素の重複をなくし一意にします
Stream<Integer> distinctStream = stream3.distinct();
distinctStream.forEach(value -> System.out.println("distinctStream: " + value));

出力結果

filterメソッド
filterStream: 3
filterStream: 3
filterStream: 6
filterStream: 6
limitメソッド
limitStream: 2
limitStream: 3
limitStream: 1
distinctメソッド
distinctStream: 2
distinctStream: 4
distinctStream: 7
distinctStream: 6
distinctStream: 5
distinctStream: 3
distinctStream: 1

※ちなみにlimitメソッドは並列のメリットが生かされず遅くなる可能性があるそうなので注意してください。

絞込みなどを行ったあとの結果を並び替えたくなるのはよくあることです。
そこで使える並び替えるメソッドもご紹介します。

メソッド 概要
sorted(Comparator comparator) Comparatorで比較し並び替えたStreamを返す

ソース

List lists = Arrays.asList(3, 1, 6, 5, 2, 4);
lists.stream().sorted().forEach(System.out::println);

出力結果

1
2
3
4
5
6

終端操作について

中間処理を経て終端操作を行う時に使うメソッドについてご紹介します。

メソッド 概要
anyMatch 一部一致判定
allMatch 全部一致判定
noneMatch 全て非合致判定

例えばフィルターで絞り込んだものからある条件に一致するものがあるか判定する場合などに使えます。
ソース

List<String> strs = Arrays.asList("とうきょう", "いばらき", "さいたま", "ちば");
System.out.println(strs.stream().anyMatch(str -> str.length() == 2));
System.out.println(strs.stream().anyMatch(str -> str.length() > 5));

出力結果

true
false

ソース

List<String> strs = Arrays.asList("とうきょう", "いばらき", "さいたま", "ちば");
System.out.println(strs.stream().allMatch(str -> str.length() >= 2));
System.out.println(strs.stream().allMatch(str -> str.length() > 4));

出力結果

true
false

ソース

List<String> strs = Arrays.asList("とうきょう", "いばらき", "さいたま", "ちば");
System.out.println(strs.stream().noneMatch(str -> str.equals("さいたま")));
System.out.println(strs.stream().noneMatch(str -> str.equals("かながわ")));

出力結果

false
true

他にも副作用(forEach,forEachOrdered)、集計(count,min,max,sum,average)などがありますがここでは紹介を省きます。

終端処理はStream内の各要素に対して、forループを最後まで(または条件一致まで)回す処理に該当します。
したがって、無限ストリームを扱う場合は無限ループにならないようにlimitで件数を制限するなど気をつけることが必要です。
また終端操作は一度しか実行できない点についても理解しておく必要があります。
終端処理を実行した後のStream変数に対する中間操作・終端操作は無効となり、実行時に例外が発生します。

ソース

List<Integer> lists = Arrays.asList(3, 1, 6, 5, 2, 4);
Stream<Integer> filterStream = lists.stream();
boolean b1 = filterStream.anyMatch(s -> s.equals(5));
System.out.println(b1);//5はあるのでtrueになる
// 要素を3の倍数でフィルターします
Stream<Integer> filterStream2 = filterStream.filter(value -> value%3 == 0);
filterStream2.parallel();
filterStream2.forEach(value -> System.out.println("filterStream: " + value));

出力結果

true
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.(Unknown Source)
at java.util.stream.ReferencePipeline.(Unknown Source)
at java.util.stream.ReferencePipeline$StatelessOp.(Unknown Source)
at java.util.stream.ReferencePipeline$2.(Unknown Source)
at java.util.stream.ReferencePipeline.filter(Unknown Source)
at casley.helloworld.main(StreamTest.java:6)

上記は中間処理ですが、終端処理でも同じく例外が発生します。

Streamを使って楽になろうぜ!

Streamを使い始めるとすっきりしたコードになり、非常にコードが理解しやすい構造になります。

Streamを利用した処理の良い所は、何の処理でどういうデータになっているか高い可読性を発揮する点にあります。
これがfor文の場合は、全体を見渡さないと、どのような結果になるのかわからない事があります。
ですのでバグを生み出しやすいfor文より今回導入されたStreamを積極的に使うことで、より不具合の少ない簡潔で分かりやすいコードが記述できると思います。

最後に

さて、ここまででStreamについての記述方法が分かってきたと思いますが、順序が重要な処理は当然あるので並列化できるところは意外と少ないかもしれません。
また並列化による処理スピードアップの代償に並列化特有のバグに悩まされる事もあるでしょう。

そこでこんな考えはいかがでしょうか?
『for文を使わずストリームAPIに置き換えることから始る』
とりあえずStreamで実装しておけば、いつでも並列に変更出来るので柔軟な対応ができるとともに、
ラムダ式に慣れる為の修行となるでしょう。

Streamとラムダ式には密接な関係があります。
for文をStream APIにしていく過程でいつの間にかにラムダ式への苦手意識がなくなってくると思います。

さて、今回はJava8の新機能StreamAPIについてご紹介いたしました。
いかがでしたでしょうか?
Streamの魅力が伝わったのならば幸いです。