こんにちは、梶原です。
これは TECHSCORE Advent Calendar 2015 の24日目の記事です。
リストの検索
Java 8 ラムダ式を利用したコレクションの処理について考える、第2回目です。
これまでの記事は以下をご覧ください。
5分で読む入門編:Java 8 ラムダ式
5分で読む入門編:Java 8 ラムダ式 コレクション編(1)リストの変換
要素の検索
特定の文字から始まる要素を抽出する場合について考えます。
まずは地名を格納したリストを作成します。
1 |
final List<String> cities = Arrays.asList("Kyoto", "Osaka", "Kobe"); |
お題目:要素を検索して新しい配列に書き出す
「K」から始まる地名を検索することにします。検索結果に該当する地名は複数存在する可能性があることを前提とします。
まずは従来の書き方として for ループ文(拡張 for 文)で書いてみた場合。
1 2 3 4 5 6 7 8 9 10 |
final List<String> startWithK = new ArrayList<>(); // 新しい配列の変数を宣言して生成! for (String city : cities) { // ループしてリストを読み込み・・・ if (city.startsWith("K")) { // 「K」から始まる場合は・・・ startWithK.add(city); // 配列に追加! } } // 結果を確認 for (String city : startWithK) { System.out.println(city); // 標準出力 } |
出力結果は以下です。
1 2 |
Kyoto Kobe |
いくつも段階を経て検索結果を取得してます。ぱっと見で「この目的を達成するための処理です!」がストレートに心に刺さるコードではないものの、やりたいことはできていますね。
では、ここから従来の記法である命令型である外部イテレータから関数型の内部イテレータへ変換させていきます。
その前に。
上記のコード中で検索を実施している if 文、いかにも「古い」という印象がありませんか?
java.util.stream の Stream インターフェースでは、検索を行う目的で filter() メソッドが提供されています。
filter() メソッドを利用するように書き換え、ラムダ式も当てはめてみます。
1 2 3 4 |
final List<String> startWithK = cities.stream() .filter(city -> city.startsWith("K")) // boolean型を返すラムダ式を期待 .collect(Collectors.toList()); // 結果をリストに変換 |
cities から “「K」から始まる” 条件に一致する地名を取得してリストに変換し、結果を格納する変数に渡す。まるで仕様をそのままコードに落とし込んだようですね。
Stream については機会があればまとめることにするので、ここでは「要素を保存しない」「元データを変更しない」「仕様をそのままコードに落とし込むことができる」仕組みを提供している、というぐらいでさらっと流します。
stream() メソッドはコレクションを Stream のインスタンスでラッピングします。
filter() メソッドの Javadoc を確認すると「このストリームの要素のうち、指定された述語に一致するものから構成されるストリームを返します。」と解説されています。戻り値が true になった場合のみ結果に追加されます。
ある条件に従って検索を行う場合には、一致する場合/一致しない場合があります。当然ですよね。
今回の例では、入力値である cities と検索結果 startWithK の要素数は一致するとは限りません。
一方、 前回 「リストの変換」に利用した map() メソッドは「このストリームの要素に指定された関数を適用した結果から構成されるストリームを返します。」と解説されています。入力値と検索結果の要素数は一致することが保証されています。
それぞれの特徴をきっちりと把握することが、安全なコードを書くことにもつながります。
要素を 1 つ検索
お題目:要素を検索して最初にヒットした要素を出力し、見つからない場合はメッセージを出力する
検索結果の要素が存在しない場合に安全に処理する方法について考えます。
今回も「K」から始まる地名を従来の記法で検索することにします。
1 2 3 4 5 6 7 8 9 10 11 12 |
String foundCity = null; for (String city : cities) { if (city.startsWith("K")) { foundCity = city; break; } } if (foundCity != null) { System.out.println(foundCity); // 標準出力:存在する場合 } else { System.out.println("Not found!"); // 標準出力:存在しない場合 } |
出力結果は以下です。
1 |
Kyoto |
やりたいことはできていますね。
ところで、「コードの臭い」に気が付いたでしょうか?
取得した地名を格納しておくために変数を宣言しています。結果を格納するためだけの変数の宣言は、例えばこの処理を並列化したい場合にバグの原因になりそうな危ない感じがします。また、null で初期化しているため「この変数は null ではないか?」という確認処理も必要になっています。
まずは、検索を実施している部分にラムダ式を当てはめて書き換えてみます。
Stream を利用すると 戻り値をそのまま final で宣言した変数に渡すことができるようになります。変数の可変性を取り除き、臭いの原因を取り除くことができます。
1 2 3 4 |
final Optional<String> foundCity = cities.stream() .filter(city -> city.startsWith("K")) .findFirst(); // 最初の要素または空 |
cities から “「K」から始まる” 条件に一致する地名を取得して最初の 1 件を結果を格納する変数に渡す。仕様をそのままコードに落とし込んだように書くことができました。
findFirst() メソッドの Javadoc を確認すると「このストリームの最初の要素を記述する Optional または空の Optional (ストリームが空の場合)を返します。ストリームが検出順序を持たない場合は、任意の要素が返されます。」と解説されています。
戻り値である特別なクラス Optional は Java8 の新機能です。コードの読み手に「結果値が返されない場合がある」ことを明確に伝えます。
orElse() や ifPresent() といったメソッドで結果値の有無に応じて処理を変更することができます。
結果値の有無に応じた標準出力を実施する部分を書き換えてみます。
1 |
System.out.println(foundCity.orElse("Not found!")); |
古い記法では「この変数は null ではないか?」という確認処理から始まっていた部分が、たった 1 行で書けています。
orElse() メソッドは、値が存在しない場合に何を返すのかを提案しています。
出力結果は「Kyoto」です。もしも「K」から始まる地名が存在しなかった場合には「Not found!」が出力されます。
今回のお題目では「見つからない場合はメッセージを出力する」が求められているので orElse() メソッドを利用しましたが、「存在する場合だけ標準出力する」が求められていたのであれば ifPresent() メソッドが利用できました。
1 |
foundCity.ifPresent(System.out::println); |
標準出力処理はメソッド参照で記載してみました。
出力結果は「Kyoto」です。もしも「K」から始まる地名が存在しなかった場合には標準出力処理は実行されません。
安全な保守を可能にするための工夫
「リストの検索を考える」という内容からは外れますが、実際にお客様に納品する場合に今回のようなコードを書くだろうか?と考えてみました。
- 同じような検索を別の箇所で実施するかも
- 検索条件が変わるかも
上記のような不安要素がコード実装時に分かっている場合は、間違いなく答えは「No」です。
発生するかもしれないあらゆる仕様変更に完璧に備えておくのは無理でも、検索条件の使いまわし・仕様変更に強いコードにしておくことは現時点で打てる手だてとしてはとても重要なものだと思います。
複数件を取得するコード例でいうと、ラムダ式で記載されている以下の部分です。
1 |
city -> city.startsWith("K") |
この部分を切り出すことができたら検索の汎用性が高くなり、仕様変更にも強くなりそうです。
ラムダ式らしい特徴に触れることができると思うので、別の機会にまとめることにします。
まとめ
- filter() メソッドは検索処理に適している
- findFirst() メソッドは 1 件検索処理に適している
- Optional クラスは検索結果が存在しない可能性がある場合に便利
勉強に利用している教科書
O'Reilly Japan - Javaによる関数型プログラミング(Venkat Subramaniam 著)(株式会社プログラミングシステム社 翻訳)オライリージャパン
Javaプログラマーなら習得しておきたい Java SE 8 実践プログラミング(Cay S. Horstmann 著)(柴田 芳樹 翻訳)インプレス
Java逆引きレシピ(竹添 直樹,高橋 和也,織田 翔,島本 多可子 著)翔泳社