Javaプログラミング ラムダ式とStream API¶
Java SE 8で導入されたラムダ式と、ラムダ式を使用するコレクションAPI(Stream API)を使うとかなり高レベルなデータ処理ができるようになりますが、そのプログラミングはパラダイムシフトを少し必要とします。慣れるのに練習が必要と思われます。
メモ集¶
Lamda道場¶
- Javaプログラミング Lambda道場
2013年秋のJJUG CCC Project Lambdaハンズオンの内容です。
Streamの生成¶
コレクション(Mapを除く)からStreamの生成¶
Javaのコレクションフレームワークでは、基底となるインタフェースCollection型にStreamを返すstreamメソッドが用意されています。
コレクション(Map)からStreamの生成¶
MapはCollectionを継承していないので、streamメソッドがありません。というより、Mapはストリーム(列)をなしていないので、entrySetを取り出してstreamを生成するのかと思います。
Map countries = ...
countries.entrySet().stream()
.map(e -> e.getValue().getName())
.forEach(System.out::println);
配列をStreamにする¶
ArraysからStreamの生成¶
Arrays.stream(args)
でStreamを生成できます。引数に指定した型により、いくつか戻り値のパターンがあります。
戻り値型 | 引数の型 | 戻り値型 | 引数の型 | |
---|---|---|---|---|
IntStream | int[] | DoubleStream | double[] | |
LongStream | long[] | Stream<T> | T[] |
- byte[]はこの方法ではStreamを生成できません
ので、次のofでStreamを生成します。
byte[] data = ...
IntStream.range(0, data.length)
.map(i -> data[i])
...
Streamのofで生成¶
Streamクラスのstaticメソッド of は、可変長引数で受けた引数のStreamを生成します。可変長引数は配列として扱われるので、ofメソッドに配列を渡すことでStreamを生成することができます。
Point[] points = ... Stream.of(points).forEach(...);
また、プリミティブ型に対応するIntStream、LongStream、DoubleStreamのそれぞれのofメソッドで、プリミティブ型の配列からStreamを生成します。
整数のStream生成¶
IntStream.range(1, 10)
1から9までの整数の順列のStreamを生成できます。
new Random().ints(-5, 5)
-5以上5未満の整数の無限Streamを生成します。
IntStream.iterate(10, i -> i + 10)
10から開始し、20, 30, ・・・と10ずつ増える整数の無限Streamを生成します。
EnumerationからStreamの生成¶
Javaの古いAPIでは、java.util.Enumeration をリストのように扱うものがあります。このEnumerationからStreamを生成する例です。
Enumeration<String> enu = ... ;
Stream<String> str = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(enu.asIterator(), Spliterator.ORDERED), false
);
- java.util.CollectionsのlistメソッドにEnumerationを渡すことでListが生成され、そこからStreamを生成することもできますが、一度すべての要素をメモリに展開するのでEnumerationが大きいときなどに計算リソースが割かれてしまいます。
- Spliteratorsには、spliteratorメソッドとspliteratorUnknownSizeメソッドがあり、前者はあらかじめイテレータのサイズがわかるとき、後者はイテレータのサイズが不明のときに使います。
- Enumerationは、asIterator()でイテレータに変換できるので、spliteratorUnknownSizeに渡すことができます。
Streamの並列化(Parallel)¶
Streamに対してparallel()を呼ぶことで、処理を並列化できます。
オーバーヘッドとトレードオフ¶
Streamを並列化するには初期オーバーヘッドが発生します。おおよそ100μ秒といわれています。
- 処理時間が100μ秒以下なら並列化すべきではない
N × Q >= 10,000
なら並列化を考慮する- Nは要素数
- Qは要素あたりの処理コスト(整数加算を1とした)
- 3次元デカルト座標系の距離計算=7、3次元デカルト座標系の大きさ計算(平方根処理なしの距離計算)=3.3
- Streamのソースは分割可能で要素の処理は独立している
コード¶
ラムダ式¶
Stream APIを使わないラムダ式によるコード例は次のページに独立。検索¶
UNIXコマンドのgrep的な処理です。
anyMatch¶
com.sun.javadoc.Tag
クラスの配列から、text()
で取り出した文字列が"1.8"、"8"、または"JDK1.8"のいずれかの文字列と一致したらtrue
を、いずれとも一致しなければfalse
を返すメソッドを記述します。
従来の書き方 | Stream APIを使った書き方 |
|
|
distinct¶
1件1行で出力されたログファイルがあり、各行は先頭にリクエスト元のクライアントのIPアドレスと、続いて各要素が空白区切りで出力されています。
このファイルを読み、IPアドレス項を取得し、重複をなくして標準出力に出力します。
従来の書き方 | Stream APIを使った書き方 |
|
|
allMatch¶
素数を判定するコード
従来の書き方 | Stream APIを使った書き方 |
|
|
データ生成¶
配列にデータを出力¶
従来の書き方 | Stream APIを使った書き方 |
|
|
文字列にデータを結合して出力¶
String text = IntStream.range(0, NUM_DATA)
.mapToObj(String::valueOf)
.collect(joining(", ", "", ""));
- Collectors.joining を使うと、文字列ストリームを、セパレーターと接頭辞・接尾辞を付けて結合した1つの文字列を終端として生成します。
- IntStream等のプリミティブ型(int)ストリームからオブジェクト型(String)にmapするときは、mapToObjを使います。
JavaFXのObserverListにデータを出力¶
従来の書き方 | Stream APIを使った書き方 |
|
|
Mapにデータを出力(groupingBy)¶
Streamに流れる要素を、要素の一部を取り出しキーとし、キーに該当する要素のリストを値とするMapとしてデータを出力します。
List<Employee> employees = ...
Map<String, List<Employee>> map = employees.stream()
.filter(Employee::isPermanent)
.collect(Collectors.groupingBy(Employee::getSection));
2つのデータ列から出力を生成¶
HttpServletのリクエストパラメータ¶
HttpServletのdoGet/doPostメソッドの実装で、HttpServletRequestからリクエストパラメータを取得し、リクエストパラメータの名前と値の組み合わせを文字列として生成します。一つの名前に複数の値が想定されます。
リクエストパラメータの取得には次のAPIを呼び出します。
Enumeration<String> getParameterNames()
String[] getParameterValues(String name)
拡張for文を使うコーディングは次となります。
List<String> names = Collections.list(req.getParameterNames());
for (String name : names) {
String[] values = req.getParameterValues(name);
for (String value : values) {
out.println("<p>" + name + "=" + value + "</p>");
}
}
Stream APIを使うコーディングは次となります。
StreamSupport.stream(Spliterators.spliteratorUnknownSize(req.getParameterNames().asIterator(), Spliterator.ORDERED),false)
.flatMap(name -> Stream.of(req.getParameterValues(name))
.map(value -> "<p>" + name + "=" + value + "</p>"))
.forEach(out::println);
まず、名前一覧のEnumerator<String>をStreamSupportクラスを使ってstreamにします。
次に、名前から取得した値一覧を別なstreamにして各要素を文字列として、flatMapで展開します。
最後に文字列を出力します。
リンク集¶
オンライン記事¶
ITPro連載:Java技術最前線¶
- 詳解 Java SE 8 第1回 Java SE 8概説
- 詳解 Java SE 8 第2回 ラムダ式
- 詳解 Java SE 8 第3回 Project Lambdaでの言語仕様変更 その1
- 詳解 Java SE 8 第4回 Project Lambdaでの言語仕様変更 その2
- 詳解 Java SE 8 第5回 Stream API その1
- 詳解 Java SE 8 第6回 Stream API その2
- 詳解 Java SE 8 第7回 Stream API その3 flatMapとcollect
- 詳解 Java SE 8 第8回 Stream API その4 collectを深掘りする
- 詳解 Java SE 8 第9回 Stream API その5 その他の中間操作
- 詳解 Java SE 8 第10回 Stream API その6 その他のメソッド
- 詳解 Java SE 8 第11回 nullチェックとOptional
- 詳解 Java SE 8 第12回 Stream API その7 Optionalクラスを使用するメソッド
- 詳解 Java SE 8 第13回 Stream APIのまとめ その1
- 詳解 Java SE 8 第14回 Stream APIのまとめ その2
- 詳解 Java SE 8 第15回 Streamと処理の遅延
- 詳解 Java SE 8 第16回 パラレルストリーム
- 詳解 Java SE 8 第17回 パラレルストリームのしくみ
- 詳解 Java SE 8 第18回 Project LambdaによるAPIの変更
セミナー資料¶
ブログから¶
- Java8のラムダ式とStream APIを試してみる
C#とJavaのラムダ式、Stream APIとLINQの違いをコードを対比しながら紹介