Java8超入門 - for文の代わりにStreamを使おう(1)
今年の3月にリリースされたJava8では、関数型インターフェース、ラムダ式、Stream
、Optional
など、関数型言語の仕組みが導入された。
このブログでも、これまで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では、既存のコレクション(List
やSet
など)とは別に、関数型プログラミングをサポートする集合データを新たに提供しており、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
文のロジックをStream
のanyMatch
メソッドを使って書き換える例を紹介した。
Stream
は、anyMatch
の他にもallMatch
やnonMatch
などの類似したメソッドに加えて、変換や抽出処理、集計など多くの機能を提供している。
これらの詳しい内容に関しては、以下の記事を参考にしてもらいたい。
Java8 Stream APIの基本(1) - 代表的な中間操作
Java8 Stream APIの基本(6) - 終端操作の概要
Java8 Stream APIの基本(7) - 終端操作 2(Stream#collect)