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

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

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

今年の3月にリリースされたJava8では、関数型インターフェース、ラムダ式StreamOptionalなど、関数型言語の仕組みが導入された。
このブログでも、これまでJava8の新機能の解説記事をたくさん書いてきたが、具体的にどんな場面で使えばいいのだろう、と素朴に疑問を感じている人もいるかもしれない。そこで、今回から何回かに分けて、実際のプログラミングですぐに使えるJava8の新機能を紹介していく。

まずはStreamの基本的な使い方から始めよう。

コレクションをfor文で操作する

ここでは、シンプルな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は省略 */
}

このPersonオブジェクトを格納したコレクションの中に、未成年のPersonが存在するかどうかをチェックするコードを書いてみよう。従来のfor文を使って書くと、次のようになるだろう。

  // 未成年が存在するかどうかを判定する
  public boolean existsInfant(List<Person> persons) {
    for (Person person : persons) {
      if (person.getAge() < 20) {
        return true;
      }
    }
    return false;
  }

なんということはない。forループを使ってコレクションからPersonオブジェクトを取り出し、未成年の条件に一致するかどうかを判定する。該当した場合には、ループの途中でも即座にtrueをリターンし、ループの最後まで到達した場合はfalseをリターンするだけだ。

for文のロジックをStreamで書き換える

Java7なら、上記のコードには何の問題もない。

しかしJava8を使うなら、このコードはもっとシンプルに書ける。Streamを使えば、以下に示すようにロジックの本体を1ステートメントで記述できる。

  public boolean existsInfant2(List<Person> persons) {
    return persons.stream()
                  .anyMatch(p -> p.getAge() < 20);
  }

簡単に解説しよう。

2行目では、コレクションのpersonsに対してstream()メソッドを呼び出すことで、Streamオブジェクトを生成している。
Streamは、Java8が提供する集合データだ。Java8では、既存のコレクション(ListSetなど)とは別に、関数型プログラミングをサポートする集合データを新たに提供しており、Streamがこれに相当する。

3行目では、取り出したStreamオブジェクトに対して、続けてanyMatchメソッドを呼び出すことで、Personデータ集合の中に該当する条件(=ここでは未成年かどうか)の要素が存在するかどうかを判定している。

anyMatchは、Streamに格納された要素のいずれか(=any)が、指定された条件に該当する(=match)かどうかを判定するメソッドだ。

ちなみに、このように戻り値つきのメソッド複数連続して呼び出すことを、「メソッドチェーン」(Method Chaining)と呼ぶ。

ラムダ式で集合データに対する処理内容を記述する

anyMatchメソッドの引数に指定しているのがラムダ式だ。ラムダ式の部分を取り出すと次のようになっている。

p -> p.getAge() < 20

Java7以前に慣れ親しんだプログラマーにとって、このコードは取っつきづらいかもしれない。何も前提知識がなければ、型宣言もなく唐突に出てくるp変数や、->記号などは意味不明に見えることだろう。

上のラムダ式をもう少し丁寧に書くと次のようになる。

(Person person) -> (person.getAge() < 20)

ここでは3つの変更を施した。

1つめは->記号の左右の記述をそれぞれ括弧でくくったことである。これにより->記号を境にして、左辺と右辺に分離できることを明示した。左辺は引数の宣言で、右辺はその引数を使った実際の処理を表している。

2つめの変更点は、先ほどはpだった変数名をpersonに変えたことである。こうすることで、ここで処理対象にしているのがPersonオブジェクトであることを明示した。実際には変数名は何でもかまわないが、一般的にラムダ式の内部では、文字列ならs、整数ならiのように短い変数名を使うのが一般的である。そのため、先ほどのコードでは変数名をpとした。

3つめは、person変数がPerson型であることを明示したことである。anyMatchメソッドは、それが呼ばれた時点でStreamに格納されている要素を処理対象にする約束になっている。この例ではanyMatchが呼ばれた時点のStreamにはPersonオブジェクトが格納されているため、変数の型を明示する必要がない。
ちなみに、このように型を明示的に宣言しなくても、コンパイラや実行環境が型を特定してくれる仕組みを「型推論」(type inference)と呼ぶ。

さて、改めて2つのラムダ式を眺めて見て欲しい。
ここまでの解説で、これらのラムダ式が、anyMatchメソッドの条件として、「Personオブジェクトに対して、person.getAge() < 20が成り立つかどうか」を指定していることが読み取れるのではないだろうか。

ラムダ式で関数型インターフェースのロジックをその場で定義する

ここで、ラムダ式がいったい何者なのかを説明しておこう。

ラムダ式は、関数型言語(および数学)の用語で、無名関数(名前を持たない関数)を定義する記法だ。

Java8においては「関数型インターフェース」に定義されたメソッドのロジックをその場で定義する仕組みを指す。

先ほどのanyMatchメソッドシグニチャJavadocで調べると、Predicateという関数型インターフェースを引数として受け取っていることがわかる。
関数型インターフェースは、Java8から新たに提供された仕組みで、基本的にメソッドを一つだけ定義できるインターフェースである。(defaultメソッド複数持てるが、その話は省略する)。

Predicateは条件判定用の関数型インターフェースのため、メソッドの戻り値はbooleanになっている。先ほどのコード例では、未成年かどうかを判定するロジックをPredicateが持つメソッドのコードとして記述したことになる。

関数型インターフェースは便宜的な仕組み

この記事の冒頭で、Java8が関数型言語の仕組み導入したことを書いた。

しかし、上で説明した「関数型インターフェース」は、一般的な関数型言語には備わっていない、Java固有の仕組みである。

関数型言語は、その名の通り「関数」を基本としてプログラムを組み上げるのが基本的な考え方である。
しかし、Javaオブジェクト指向言語として考案されたため、オブジェクトに属さない関数を定義する仕組みを持っていなかった。
そんなJavaに、あとから関数型言語の仕組みを取り入れるために、「メソッドを1つだけ持つ関数型インターフェース」を導入することで、既存の仕組みを大きく変えずに対応した経緯がある。

このため、ラムダ式を書く際には、関数型インターフェースのことは深く考えず、純粋にそこで必要となる関数(Javaではメソッド)のロジックを定義するものと考えれば良いだろう。
おそらくJava8の設計者も、人々がそんな風に考えてプログラミングすることを期待しているはずである。

Streamを使うとコードが簡潔で読みやすくなる

最後にStreamを使うメリットをまとめておこう。

メリットを一言で言えば、コードが簡潔になり、読みやすくなることだ。

一般的に、for文よりもStreamを使う方がコードの行数は短くなる。
より重要なのは可読性の高さだ。

最初に示したfor文のコードを改めて見てもらいたい。このコードでは、ループの中で1要素ずつPersonが未成年かどうかを判定し、見つかった場合には即座にtrueを返し、最後まで見つからなかった場合にはfalseを返すことで未成年の存在チェックを行っている。

一方で、Streamを使ったコードでは、anyMatchメソッドの中に、未成年の条件を記述している。 Streamに格納された要素のいずれか(=any)が、カッコ内に書いた条件に該当する(=match)かどうかを判定する、というロジックを簡潔に記述できていることが読み取れるだろう。

まとめと関連記事

今回の記事では、for文のロジックをStreamanyMatchメソッドを使って書き換える例を紹介した。

Streamは、anyMatchの他にもallMatchnonMatchなどの類似したメソッドに加えて、変換や抽出処理、集計など多くの機能を提供している。 これらの詳しい内容に関しては、以下の記事を参考にしてもらいたい。

Java8 Stream APIの基本(1) - 代表的な中間操作
Java8 Stream APIの基本(6) - 終端操作の概要
Java8 Stream APIの基本(7) - 終端操作 2(Stream#collect)

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