ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識:Java 8はラムダ式でここまで変わる(3)(3/3 ページ)
本連載では、今までJavaの経験はあっても「ラムダ式は、まだ知らない」という人を対象にラムダ式について解説していきます。今回は、Java 8の新機能として、汎用的に使える関数型インターフェースの概要とパターン、Stream APIの特徴と種類3つの注意点についてコード例を交えて解説。
Stream APIの並列処理はマルチコアの恩恵を受けて高速化できるのか
Stream APIの特徴の1つに並列処理の実装を容易にできることがあります。現在、多くのコンピューターのCPUは複数のコアを持っています。そして、それらの各コアが処理を同時に行うことで負荷を分散し処理速度を上げています。
しかし、単純なforループのように処理を1つのスレッドで順々に行う直列処理の場合、それらの恩恵を受けることができませんでした。一方で、並列処理を使えるようになれば、複数コアのCPUの利点を生かしたパフォーマンスの改善が期待できるようになります。
streamメソッドをparallelStreamメソッドに書き換えるだけ
では、実際にStream APIを使って並列処理の実装を行ってみましょう。先ほどのStream APIサンプルでは並列処理ではなく直列処理を行っています。並列処理にするには、このCollectionのstreamメソッドをparallelStreamメソッドに書き換えるだけで並列処理に変更できます。
今回のサンプルを並列処理に変えると、次のようになります。
int maxSalesAmount = salesDataList.parallelStream() ←streamをparallelStreamに変更 .filter(salesData -> salesData.getLocation().equals("東京")) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt();
実際に試してみた
マルチコアのCPUのマシンで並列処理を行うと本当に効果があるのか実際に試してみましょう。今回は上記の処理に2014年1月以前という条件を加えて、次の3パターンで5回実行してみました。
- forループで行った場合
- streamメソッドで行った場合
- parallelStreamメソッドで行った場合
実装は次のようになります。
public static void main(String[] args) { // 2014年1月末日を取得 Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.set(2014, 1, 31); Date endDateOfJanuary = calendar.getTime(); // テストデータの生成 List<SalesData> salesDataList = SalesData.createSalesDateList(); // 5回実行 for (int i = 1; i <= 5; i++) { System.out.println(i + "回目 ------------------------------"); // forループの場合 int maxSalesAmount = -1; long start = System.currentTimeMillis(); for (SalesData salesData : salesDataList) { if (salesData.getLocation().equals("東京")) { if (salesData.getDate().compareTo(endDateOfJanuary) <= 0) { if (maxSalesAmount < 0) { maxSalesAmount = salesData.getSalesAmount(); } else if (maxSalesAmount < salesData.getSalesAmount()) { maxSalesAmount = salesData.getSalesAmount(); } } } } long end = System.currentTimeMillis(); System.out.println("forループ: time=" + (end - start) + "ms"); // streamメソッドの場合 maxSalesAmount = -1; start = System.currentTimeMillis(); maxSalesAmount = salesDataList.stream() .filter(salesData -> salesData.getLocation().equals("東京")) .filter(salesData -> salesData.getDate().compareTo(endDateOfJanuary) <= 0) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt(); end = System.currentTimeMillis(); System.out.println("stream: time=" + (end - start) + "ms"); // parallelStreamメソッドの場合 maxSalesAmount = -1; start = System.currentTimeMillis(); maxSalesAmount = salesDataList.parallelStream() .filter(salesData -> salesData.getLocation().equals("東京")) .filter(salesData -> salesData.getDate().compareTo(endDateOfJanuary) <= 0) .mapToInt(salesData -> salesData.getSalesAmount()) .max() .getAsInt(); end = System.currentTimeMillis(); System.out.println("parallelStream: time=" + (end - start) + "ms"); } }
これを4コアのCPUのマシンで実行したところ、次の結果になりました。
1回目 ------------------------------ forループ: time=282ms stream: time=420ms parallelStream: time=322ms 2回目 ------------------------------ forループ: time=536ms stream: time=563ms parallelStream: time=162ms 3回目 ------------------------------ forループ: time=483ms stream: time=683ms parallelStream: time=154ms 4回目 ------------------------------ forループ: time=485ms stream: time=689ms parallelStream: time=150ms 5回目 ------------------------------ forループ: time=374ms stream: time=559ms parallelStream: time=149ms
この結果を見るとStream APIの並列処理(parallelStream)を使った場合、最初の1回目は時間を取られていますが、それ以降は直列処理のforループやstreamメソッドの場合よりも速く処理が行われていることが分かります。
【注意点1】対象となる要素が少ない場合は、並列処理の方が遅くなる
しかし、並列処理にしたからといって全てのケースでパフォーマンス改善が行われるわけではありません。並列処理を行う場合、内部の処理ですべきことが多くなるため直列処理よりも大きなオーバーヘッドが発生することになります。
対象となる要素が多い場合、そのオーバーヘッドのデメリットよりも並列で処理をすることによる高速化の方がメリットは大きくなりますが、要素が少ない場合、逆にオーバーヘッドのデメリットの方が大きくなってしまい、並列処理の方が遅くなってしまうこともあります。
例えば、SalesDataのListを生成するのに日数のループを100万日から10日に減らした場合、結果は次のようになり、並列処理の方が遅くなっています(実行時間がかなり短くなるため、結果をナノ秒で算出するように変更しています)。
1回目 ------------------------------ forループ: time=408000ns stream: time=36956000ns parallelStream: time=7851000ns 2回目 ------------------------------ forループ: time=87000ns stream: time=165000ns parallelStream: time=193000ns 3回目 ------------------------------ forループ: time=78000ns stream: time=115000ns parallelStream: time=246000ns 4回目 ------------------------------ forループ: time=126000ns stream: time=173000ns parallelStream: time=216000ns 5回目 ------------------------------ forループ: time=125000ns stream: time=158000ns parallelStream: time=218000ns
そのため、やみくもに並列処理にすることでパフォーマンスの改善が図れるわけではないので、注意してください。
【注意点2】コア数によってパフォーマンスは変わる
また、コア数によって処理のパフォーマンスは変わってきます。4コアのCPUのマシンで良いパフォーマンス結果を得たからといって、2コアのCPUのマシンにそれを移植したとしても、同じように良いパフォーマンス結果を得られるというわけではありません。
【注意点3】元データの並び順は保障されない
また、並列処理を使った際の注意事項として「元となるデータがListのように並び順が保障されている集合体でも、並列処理の際はその順番通り実行されるわけではない」点があります。
例えば、次のようにforEachメソッドを使ってListの要素を標準出力する場合、直列処理だとListの順番通り出力されますが、並列処理だとListの順番通り出力されるわけではありません。
List<String> list = Arrays.asList( "list1", "list2", "list3", "list4", "list5" ); list.stream().forEach(e -> System.out.println("stream: " + e)); list.parallelStream().forEach(e -> System.out.println("parallelStream: " + e));
stream: list1 stream: list2 stream: list3 stream: list4 stream: list5 parallelStream: list3 parallelStream: list5 parallelStream: list4 parallelStream: list2 parallelStream: list1
次回はStream APIの主なメソッド
今回はJava 8で追加されたjava.util.functionパッケージの汎用的な関数型インターフェースとJava 8の特に注目されているStream APIについて見てきました。
このStream APIの概要が分かったところで、次回は実際Stream APIはどのように使われるのか主なメソッドについて見ていきます。そして、そこでラムダ式がどのように使われているのかを解説します。ご期待ください。
著者プロフィール
長谷川 智之(はせがわ ともゆき)
株式会社ビーブレイクシステムズ開発部所属。
社内サークル執筆チーム在籍。
主な執筆。
@IT連載『Javaの常識を変えるPlay framework入門』
日経ソフトウェア連載『コツコツ学ぶAndroidネイティブアプリ開発教室』
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- JavaOne Tokyo 2012まとめレポート(後編):ラムダ式、JAR脱獄、JavaScript/■■Node.jsに接近するJDK 8、そして9へ
JDK 8の新機能のうち、Lambda、Jigsaw、Nashorhについて解説した講演を詳細にレポートする。そしてJava SE 9はどうなる? - スケーラブルで関数型でオブジェクト指向なScala入門(4):基本的なパターンマッチとScalaで重要な“関数”
match構文、関数の定義と呼び出し、高階関数、プレイスホルダ構文、部分適用、クロージャなどを紹介します - 「Javaに並列処理と関数型言語の要素を」、ティム・ブレイ氏
- いよいよ始まるRuby 1.9への移行:関数型っぽくなったRuby 1.9
- WebSocketやREST APIのサポート強化:Java 8&Java EE 7に対応した「Spring Framework 4.0」正式版リリース
米Pivotalは2013年12月12日、オープンソースのJavaアプリケーションフレームワーク「Spring Framework 4.0」の正式版をリリースした。 - JDK 8、TLS 1.2がデフォルトに
Javaの通信暗号化もTSL 1.2に。基本的に後方互換性は維持するが、一部影響がある場合もあるという。 - .NET開発『虎の巻』:C#ラムダ式 基礎文法最速マスター
ラムダ式(C#)の基礎文法を、短い説明と簡単なコードで簡潔にまとめる。「ラムダ式、どう書くんだっけ?」という場合の簡易リファレンスとして活用できる - .NET開発『虎の巻』:VBラムダ式 基礎文法最速マスター
今度はVB。ラムダ式の基礎文法を、短い説明と簡単なコードでまとめる。「ラムダ式、どう書くんだっけ?」という場合の簡易リファレンスとして活用できる - 特集:C#開発者のためのF#入門(前編):F#で初めての関数型プログラミング
アルゴリズム実装などに威力を発揮する、関数型プログラミングの基礎やF#言語の特徴を解説。C#開発者なら、ここから始めよう! - Gaucheでメタプログラミング(1):ちょっと変わったLisp入門
Lispの一種であるScheme。いくつかある処理系の中でも気軽にスクリプトを書けるGaucheでLispの世界を体験してみよう