こんにちは、梶原です。
「Java 8 のコードをラムダ式をはじめとする新機能を利用してカッコよく且つ安全に書けるようになりたい」を目標に勉強中です。
前回の記事「5分で読む入門編:Java8ラムダ式」ではコードの記法の転換(「どのように実現するか」から「何を実現させるのか」)について考えました。今回はラムダ式を利用したコレクションの処理について考えます。
コレクションについては3回に分ける予定です。
- リストの変換
- リストの検索
- リストの集約・計算
1つの記事として書いてしまおうと思ったのですが、どうしても5分以内で読める量にまとめられなかったので・・・。例題のコード量も多くなりそうです。今回は1回目。量は少なめになりました。
もしも書きたいことが増えたら「番外編」「特別編」とか「最終章」で対応することになると思います。こういう緩い感じで書きますので、よろしくお願いします。
リストの変換
要素の文字列を大文字・小文字に変換して、元のデータは変更しないで新しいコレクションを作成・出力する場合について考えます。
まずは地名を格納したリストを作成します。
1 |
final List<String> cities = Arrays.asList("Kyoto", "Osaka", "Kobe"); |
お題目:この要素を1つずつ取り出して大文字に変換し、標準出力に出力する
まずは従来の書き方として for ループ文(拡張 for 文)で書いてみた場合。
1 2 3 4 5 6 7 |
final List<String> uppercaseCities = new ArrayList<String>(); for(String city : cities) { uppercaseCities.add(city.toUpperCase()); // 大文字に変換 } for(String city : uppercaseCities) { System.out.println(city); // 標準出力 } |
出力結果は以下です。
1 2 3 |
KYOTO OSAKA KOBE |
やりたいことは出来ていますね。
では、ここから従来の記法である命令型である外部イテレータから関数型の内部イテレータへ変換させていきます。for ループ文の代わりに内部イテレータ forEach() を利用するように書き換えます。
1 2 3 4 5 6 |
final List<String> uppercaseCities = new ArrayList<String>(); cities.forEach(new Consumer<String>() { public void accept(final String city) { uppercaseCities.add(city.toUpperCase()); } }); |
(標準出力処理は省略しています。)
Java8 の新機能・Iterable インタフェースに追加された forEach(Consumer
前回と同じく、ラムダ式をあてはめて関数化します。
1 2 |
final List<String> uppercaseCities = new ArrayList<String>(); cities.forEach(city -> uppercaseCities.add(city.toUpperCase())); |
(標準出力処理は省略しています。)
パラメータ・戻り値の型 <String> の記述を省略しているので、一気にコード量が減りました。
ただし、大文字に変換されたリストを格納するための変数が宣言されており、変数への要素の追加処理も書かないといけません。特に変数が宣言されている場合は、例えばこの処理を並列化したい場合にバグの原因になりそうな危ない感じがします。いわゆる「コードの臭い」です。
そこで、java.util.stream の Stream インターフェースを利用してみます。先のコード例では標準出力処理は省略しましたが次の例ではちゃんと書きます。それでも、かなりコード量が減っていることが分かると思います。
1 |
cities.stream().map(city -> city.toUpperCase()).forEach(city -> System.out.println(city)); |
cities から要素を取得 → 大文字に変換したコレクションを作成 → 標準出力する。まるで仕様を読んだそのままをコードに書いたようですね。
Stream インターフェースを利用したことで、連続して関数を呼び出す構成のコードにすることが出来ました。 Stream については機会があればまとめることにするので、ここでは「要素を保存しない」「元データを変更しない」「仕様を読むようにコード書くことが出来る」仕組みを提供している、というぐらいでさらっと流します。
stream() メソッドはコレクションを Stream のインスタンスでラッピングします。
map() メソッドは連続した入力を連続した出力に変換します。ここでは cities の各要素に大文字変換する関数を適用し、新たに作成された Stream に集約しています。また、入力値と出力値のコレクションの要素数が同じであることを保証するので、安全なコードを書く上でもぜひ利用していきたいメソッドです。なお、入力値と出力値の型は同じである必要はありません。入力値が文字列 → 出力値が文字数、という仕様にも対応できます。
続く forEach() メソッドで新たに作成されたコレクションから要素を1つずつ取得して標準出力しています。
またまた前回の記事の話になりますが、メソッド参照を利用するとコードをさらに省略できる場合があります。今回も処理が非常にシンプルなので、コード量を削減することが出来ます。要素を小文字化する例とあわせて、以下のようになります。
1 2 3 4 |
// 大文字化 cities.stream().map(String::toUpperCase).forEach(System.out::println); // 小文字化 cities.stream().map(String::toLowerCase).forEach(System.out::println); |
「テストに出る」と言われたらそのまま覚えてしまいそうなぐらいに、シンプルなコードになりました。
まとめ
- Stream インターフェースは関数を連続して適用していくコード構成にすることが出来る
- map() メソッドは入力値を変換して出力する処理に適している
勉強に利用している教科書
O'Reilly Japan - Javaによる関数型プログラミング(Venkat Subramaniam 著)(株式会社プログラミングシステム社 翻訳)オライリージャパン
Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング(Cay S. Horstmann 著)(柴田 芳樹 翻訳)インプレス
Java逆引きレシピ(竹添 直樹,高橋 和也,織田 翔,島本 多可子 著)翔泳社
最後に
次回はリストの検索について考えます。今回は map() メソッドでしたが、次回は filter() メソッドがメインです。また、NullPointerException との付き合い方が随分と変わることになりそうな java.util.Optional にも触れたいと思います。