読者です 読者をやめる 読者になる 読者になる

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

Java8超入門 - for文の代わりにStreamを使おう(2)

Java8 Stream

だいぶ間が空いてしまったが、Java8の超入門記事の第2回である。

前回の記事では、forループの代わりにStream APIanyMatchメソッドを使って、コレクション全体に対する条件判定を行う方法を紹介した。
今回は、コレクションから条件に該当する要素を抽出する際に有用な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)。

f:id:enterprisegeeks:20150603181612p:plain
図1:コレクションからStreamを生成する

実際にはこのstream()メソッドは、java.util.Collectionインターフェースに定義してあるため、ListSetをはじめとする全てのコレクションクラスで利用できる。

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メソッドを呼び出している。ここで行っているのは要素の抽出だ。

filterStreamオブジェクトから特定の要素を抽出するためのメソッドである(図2)。

f:id:enterprisegeeks:20150603175210p:plain
図2:filter()を使って条件に合う要素だけを抽出する

filterメソッドでは、Streamに格納されているそれぞれの要素に対して、指定された抽出条件を評価し、結果が真になる要素だけを抽出する。
抽出条件はfilterの引数に関数オブジェクトとして指定する。

先ほどのコードでは、ラムダ式を使って以下のように抽出条件を指定した。

p -> !p.isMale() && p.getAge() <= 2

ラムダ式の構文は「引数 -> 本体」である。
このため、上のラムダ式は、引数がpで、本体が!p.isMale() && p.getAge() <= 2である。

filterメソッドの引数に指定したラムダ式は、その時点でStreamオブジェクトに格納されている要素を処理対象にする。
今回の例の場合、filterを呼び出した時点のStreamにはPersonオブジェクトが格納されているため、上記のラムダ式pPersonオブジェクトを指している。
そして、そのPersonオブジェクトに対して、女性かどうか(=isMale()が偽か)、および2才以下かどうか(=getAge()が2以下か)を判定し、&&演算子を使って2つの条件のANDを取っている。

ちなみに前回の記事と同様に、引数名をよりわかりやすくし、括弧を追加したラムダ式は次のようになる。

(Person person) -> (!person.isMale() && person.getAge() <= 2)

このfilterメソッドのように、Streamオブジェクトを別のStreamオブジェクトに変換する操作を「中間操作」と呼ぶ。

countメソッドで要素数を数える

最後に行っているのは、countメソッドの呼び出しである。これもStreamインターフェースのメソッドで、その時点でStreamに格納されている要素の数が返る(図3)。

f:id:enterprisegeeks:20150604124217p:plain
図3:count()を使ってStream内の要素を数える

このcountメソッドは、Collectionsize()や配列の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のようになる。

f:id:enterprisegeeks:20150608142159p:plain
図4:Stream#filterによる条件抽出の全体の流れ

Stream APIに慣れていない方は、コードと図をつきあわせて、仕組みを理解してほしい。

まとめ

今回は、コレクションから条件に該当する要素を抽出するサンプルを通じて、filterメソッドを中心にStream APIの基本的な仕組みを解説した。

次回mapメソッドを紹介する。