StreamAPIのお勉強 フィルター処理とか特定の値を取得したりとか


Stream APIやらラムダ式やらの勉強を始めて二か月ぐらいが経ったので、業務でよく使う処理をまとめてみた。
中間処理やStream APIって何?みたいな前提はある程度知っている人向けに書いています。
また自分の勉強用に書いたので間違っている箇所があればご指摘お願いします。

従来のnullチェック

例えばリスト内の文字列を小文字に変換したいという処理があったとする。
その各文字列の中にnullがあった場合は当然for文の中などでフィルターをかける(nullチェックする)必要がある。
従来の記述方法で書いてみた。

従来のnullチェック
final List<String> list = Arrays.asList( "AAA", null, "CCC" );  // サンプルリスト

// フィルター前
System.out.println( "フィルター前リスト:" );
list.stream().forEach( System.out::println ); // リスト内の文字列を出力

final List<String> filterList = new ArrayList<String>();
for( final String o : list )
{
    if( o != null )
    {
        // 文字列を小文字に変換後、フィルター後リストに追加
        filterList.add( o.toLowerCase() );
    }
}

// フィルター後
System.out.println( "\nフィルター後リスト:" );
filterList.stream().forEach( System.out::println );
出力結果
フィルター前リスト:
AAA
null
CCC

フィルター後リスト:
aaa
ccc

これをStream APIで書くと以下のようになる。

Stream APIでnullチェック

StreamAPIでnullチェック
final List<String> list1 = Arrays.asList( "AAA", null, "CCC" );  // サンプルリスト

// フィルター前
System.out.println( "フィルター前リスト:" );
list1.stream().forEach( System.out::println );

final List<String> filterList1 = list1.stream()
                                      .filter( o -> o != null )           // null以外を抽出
                                      .map( o -> o.toLowerCase() )        // nullは上のfilterで除外されているのでヌルポの心配なし!
                                      .collect( Collectors.toList() );

// フィルター後
System.out.println( "\nフィルター後リスト:" );
filterList1.stream().forEach( System.out::println );
出力結果
フィルター前リスト:
AAA
null
CCC

フィルター後リスト:
aaa
ccc

filter処理で各文字列のフィルターをかけ、map処理で各文字列の変換を行う。filter処理の返り値はboolean型なので最終的にtrueかfalseを返せれば良い。trueを返せばそのオブジェクトを抽出でき、falseの場合は除外される。
filter( o -> o != null )」の「o != null」がフィルター条件部分。「o ->」という見慣れない記述は、例でいうとリスト内の各オブジェクトを指す名前(例で言うとList内のString型の各オブジェクト)。
これは自由に付けられるので例えば「filter( str -> str != null )」のような形でも良い。

最後のcollect処理ではList型に変換する処理を行っている。mapまでの書いた状態だとStream型を返すのでそれをList型にする。

※特に記述をしていなかったが途中に出力として書いていた「System.out::println」のような記述方法は「メソッド参照」と言う。
上記のmap処理をメソッド参照に置き換えるとさらに記述を縮めることができて「(変数名) -> 変数名.メソッド名()」を「クラス名::メソッド名」と書くことができる。例えば「map( o -> o.toLowerCase() )」を「map( String::toLowerCase )」のように置き換えて書くことができる。

追記:
※「filter( o -> o != null )」は以下のように簡潔に書くことができる。
 「filter( Objects::nonNull )

Stream APIでnullチェック(複数行)

各処理では単一行ではなく複数行で書くこともできる。
複数行で書くときは「o -> 処理」を「o -> { 処理; }」のように記述する。
複数行なのでちゃんと「;」を付けなくてはいけなかったり、処理によってはreturn文が必要だったりして長くなりがちになるので個人的には必要に迫られたとき以外はあまり使いたくないと考えている。

上記で行った処理を複数行で書くと以下のように書き直すことができる。

StreamAPIでnullチェック(複数行)
final List<String> filterList1 = list1.stream()
                                      .filter( o -> { 
                                                      if( o != null )
                                                      {
                                                          return true;
                                                      }
                                                      else
                                                      {
                                                          return false;
                                                      }
                                                    }
                                             )           // null以外を抽出
                                      .map( o -> o.toLowerCase() )        // nullは上のfilterで除外されているのでヌルポの心配なし!
                                      .collect( Collectors.toList() );

これのおかげでかなり複雑な処理も対応できる点が魅力的。
人によってはfor文のループを全てこれに置き換えてforeach処理に入れている人もいるが以下の注意が必要。

  • チェック例外処理はラムダ式内で拾わなければならない。

  • 内部のブロックで宣言した変数は実質finalとして扱われる。ラムダ式外の変数を利用する時もfinalとして扱われ代入することができない。

  • インデントのしかたによっては逆に可読性が落ちる。

特にチェック例外についてがかなり曲者で、ラムダ式内で例外をスローした場合はその中で処理をしなければならない。

final List<String> filterList2 = list2.stream()
                                      .filter( o -> { 
                                                      if( o != null )
                                                      {
                                                          return true;
                                                      }
                                                      else
                                                      {
                                                          throw new Exception();
                                                      }
                                                    }
                                             )           // null以外を抽出
                                      .map( o -> o.toLowerCase() )        // nullは上のfilterで除外されているのでヌルポの心配なし!
                                      .collect( Collectors.toList() );

上記の処理とあまり変わらないが、filter処理の条件でリスト内のオブジェクトがnullの時に無理やりExceptionをスローしている。
このような処理を書いた場合、この式の中で例外をキャッチしなさいというようなエラーが出る。
対処法はチェック例外をRuntimeExceptionでラップしてあげてラムダ式外にスローするかチェック例外をスローする自作の関数インターフェースを作り飛ばすという方法などがある。

これについては下記のURLが詳しい。
Java Stream API とチェック例外(検査例外)の相性が悪い件
うちはここから飛んだ先にあるLambdaExceptionUtilを使わせてもらっている。
リンク先ではチェック例外を殺して全てExceptionになると書いてあるが、例外をExceptionでキャッチしたあとその中のif文でinstanceof比較すれば一応それぞれのチェック例外を取れなくもない・・けどあまりよろしくはないのかも?(そうやっている人を見たことがない為)

フィルターに一致した特定のオブジェクトのみを取り出す

上記まではList型の内容をフィルターし、List型として取得していた。
List型ではなくフィルターした結果のString型そのものを取得したい場合の話。
フィルターした結果をfindFirstというメソッドを使って最初に見つかったオブジェクトを抽出する。
従来の処理とStream APIの処理の例を以下に書いた。

従来のリストから一致したものを返す処理
final List<String> list = Arrays.asList( "AAA", null, "CCC" );  // サンプルリスト

System.out.println( "取得前リスト:" );
list.stream().forEach( System.out::println ); // リスト内の文字列を出力

String filterStr = "";
for( final String o : list )
{
    if( "CCC".equals( o ) )
    {
        filterStr = o;
    }
}
System.out.println( "\n取得結果:" + filterStr );
StreamAPIのリストから一致したものを返す処理
final List<String> list = Arrays.asList( "AAA", null, "CCC" );  // サンプルリスト

System.out.println( "取得前リスト:" );
list.stream().forEach( System.out::println ); // リスト内の文字列を出力

final String filterStr = list.stream()
                             .filter( o -> "CCC".equals( o ) )
                             .findFirst()
                             .orElse( "" );
System.out.println( "\n取得結果:" + filterStr );
出力結果
取得前リスト:
AAA
null
CCC

取得結果:CCC

上記のstream処理では"CCC"という文字列がリスト内に存在したらその文字を抽出して返す処理。
もし存在しなかった場合の処理はorElse()の中に書いてある文字列を取得する。
findFirst()のみの場合はオブジェクトをOptionalという型でラップされたものが返るので、orElseやorElseGetでその中身を取得する。
このOptionalという型はnullが入っている可能性のある変数に対して、明示することができる非常に強力な型なので機会があれば別に紹介したい。

ソート処理

ソートについて書こうと思ったけどこのサイトがとても分かりやすいのでここを見れば大体のソートはできると思う。
(o1, o2) -> o1 - o2 なんて呪文はもうやめて! - Java8でのComparatorの使い方

終わり

Stream APIはもっといろいろな機能があるけどとりあえずこれさえ覚えておけばある程度は戦えると思う!
Stream APIを勉強していてfinalを多用するようになったり、for文を見るとstreamに置き換えたくなったりといままでのコードを見直す機会にもなったのでとても良い勉強になった。
collect辺りがまだよくわかってないので、まだまだ理解を深めていこうと考えている。