Java8超入門 - for文の代わりにStreamを使おう(2)
だいぶ間が空いてしまったが、Java8の超入門記事の第2回である。
前回の記事では、for
ループの代わりにStream APIのanyMatch
メソッドを使って、コレクション全体に対する条件判定を行う方法を紹介した。
今回は、コレクションから条件に該当する要素を抽出する際に有用なfilter
メソッドを紹介する。
前回と同様に、シンプルなPerson
クラスを利用する。
public class Person { private String name; private boolean maleFlag; private int age; public String getName() { return this.name; } public boolean isMale() { return this.maleFlag; } public int getAge() { return this.age; } /* コンストラクタやsetterは省略 */ }
for
ループによるコード
このPerson
オブジェクトを格納したコレクションから、ある条件を満たすPerson
の数を数えるコードを書いてみよう。
例として、2才以下の女児の人数を数えるコードをfor
文を使って書くと次のようになるだろう。
public long countBabyGirls(List<Person> persons) { long count = 0; for (Person person : persons) { if (!person.isMale() && person.getAge() <= 2) { count++; } } return count; }
シンプルなコードである。最初に人数を集計するcount
変数を用意して、for
ループの中で条件に合致する要素が見つかった場合にカウントアップし、最後にリターンするだけだ。
Stream APIによる書き換え
このコードも、Java8で提供されたStream APIを使うことで、もっと簡潔に記述できる。
public long countBabyGirls2(List<Person> persons) { return persons.stream() .filter(p -> !p.isMale() && p.getAge() <= 2) .count(); }
最初に全体の流れを簡単に説明しておこう。
2行目では、引数のpersons
変数に格納されたPerson
コレクションに対して、stream()
メソッドを呼び出してStream
オブジェクトを作っている。
続く3行目では、そのStream
オブジェクトに対してfilter
メソッドを呼び出して、条件に合致する要素だけを抽出している。このfilter
メソッドも戻り値はStream
オブジェクトである。
4行目では、Stream
オブジェクトに対してcount
メソッドを呼び出して、要素の数を求めてリターンしている。
以降では、このコードをもう少し詳しく説明していく。
コレクションからStream
オブジェクトを生成する
最初に行っているのは、コレクションに対するstream()
メソッドの呼び出しである。この部分だけを独立して書くと次のようになる。
Stream<Person> personStream = persons.stream();
上のコードを見るとわかるように、stream()
メソッドの戻り値はStream
オブジェクトである。
ここでは、コレクションに対してstream()
を呼び出すことで、そのコレクションの要素を持つStream
オブジェクトを作っている(図1)。
実際にはこのstream()
メソッドは、java.util.Collection
インターフェースに定義してあるため、List
やSet
をはじめとする全てのコレクションクラスで利用できる。
package java.util; public interface Collection<E> extends Iterable<E> { default Stream<E> stream() { // ロジックは省略 }
Collection
はインターフェースだが、stream()
メソッドにはJava8から提供されたdefault
修飾子が付いているため、ロジックも記述してある。
このstream()
メソッドのように、Stream
オブジェクトを最初に作る操作を「生成操作」と呼ぶ。
Stream
はJava8が提供する集合データ
Stream
はJava8から提供された集合データである。
Stream<T>
と、型パラメーターを伴ったテンプレートクラスとして定義されているため、Javaオブジェクトなら何でも格納できる。
JavaはもともとList<E>
やSet<E>
などのコレクションクラス群を提供してきたが、Java8からは並列処理も含めた関数型プログラミング機能や無限ストリームの機能を提供するために、このStream
インターフェースを導入した。
filter
で条件に合致する要素だけを抽出する
さて、先ほどのコードに戻ろう。
3行目では、Stream
オブジェクトに対してfilter
メソッドを呼び出している。ここで行っているのは要素の抽出だ。
filter
はStream
オブジェクトから特定の要素を抽出するためのメソッドである(図2)。
filter
メソッドでは、Stream
に格納されているそれぞれの要素に対して、指定された抽出条件を評価し、結果が真になる要素だけを抽出する。
抽出条件はfilter
の引数に関数オブジェクトとして指定する。
先ほどのコードでは、ラムダ式を使って以下のように抽出条件を指定した。
p -> !p.isMale() && p.getAge() <= 2
ラムダ式の構文は「引数 -> 本体
」である。
このため、上のラムダ式は、引数がp
で、本体が!p.isMale() && p.getAge() <= 2
である。
filter
メソッドの引数に指定したラムダ式は、その時点でStream
オブジェクトに格納されている要素を処理対象にする。
今回の例の場合、filter
を呼び出した時点のStream
にはPerson
オブジェクトが格納されているため、上記のラムダ式のp
はPerson
オブジェクトを指している。
そして、そのPerson
オブジェクトに対して、女性かどうか(=isMale()
が偽か)、および2才以下かどうか(=getAge()
が2以下か)を判定し、&&
演算子を使って2つの条件のANDを取っている。
ちなみに前回の記事と同様に、引数名をよりわかりやすくし、括弧を追加したラムダ式は次のようになる。
(Person person) -> (!person.isMale() && person.getAge() <= 2)
このfilter
メソッドのように、Stream
オブジェクトを別のStream
オブジェクトに変換する操作を「中間操作」と呼ぶ。
count
メソッドで要素数を数える
最後に行っているのは、count
メソッドの呼び出しである。これもStream
インターフェースのメソッドで、その時点でStream
に格納されている要素の数が返る(図3)。
このcount
メソッドは、Collection
のsize()
や配列のlength
プロパティと同じ位置づけなので、わかりやすいだろう。
先ほどのstream()
やfilter()
メソッドの場合、戻り値のStream
に対してさらに続けてメソッド呼び出しができる。しかし、このcount()
メソッドは戻り値がlong
のため、これ以上メソッド呼び出しを連鎖させることはできない。
このように、Stream
に対する一連の操作を終わらせるメソッドを「終端操作」と呼ぶ。
全体の流れ
最後に、Stream APIを使った今回のサンプルコードを再掲しておく。
public long countBabyGirls2(List<Person> persons) { return persons.stream() .filter(p -> !p.isMale() && p.getAge() <= 2) .count(); }
全体の流れを図にまとめると、図4のようになる。
Stream APIに慣れていない方は、コードと図をつきあわせて、仕組みを理解してほしい。
まとめ
今回は、コレクションから条件に該当する要素を抽出するサンプルを通じて、filter
メソッドを中心にStream APIの基本的な仕組みを解説した。