Java SE 8のストリームAPIの正しい使い方──ラムダ式とともに導入された新APIで、並列処理の実装はどう変わるのか?

Oracle Java & Developers編集部
2014-11-28 11:00:00
  • このエントリーをはてなブックマークに追加

Java SE 8では、ラムダ式の導入に伴い、並列処理の実装を容易にする新APIとして「ストリームAPI」が追加された。その効果的な使い方を、前回の記事に続いて米国オラクルのスチュアート・マークス氏が解説する。

>> 前回の記事はこちら

米国オラクルJavaプラットフォーム・グループ テクニカル・スタッフ プリンシパル・メンバーのスチュアート・マークス氏
米国オラクルJavaプラットフォーム・グループ テクニカル・スタッフ プリンシパル・メンバーのスチュアート・マークス氏

 前回は、米国オラクルでJava SEの仕様策定をリードするスチュアート・マークス氏によるラムダ式の基礎解説をお届けした。今回はその続編として、Java SE 8でラムダ式のサポートに伴って導入された新機能「ストリームAPI」に関するマークス氏の解説をお届けする。

※本記事は、2014年5月に東京で開催された「Java Day Tokyo 2014」におけるスチュアート・マークス氏のセッション「Lambda式とストリームAPI、並列処理の詳細」を基に構成しています。

パイプラインや並列処理をスマートに実装できるJava SE 8の新機能「ストリームAPI」

 Java SE 8では、言語仕様のほかにクラス・ライブラリに関してもさまざまな拡張が行われていますが、その中で目玉となるものの1つが「ストリームAPI(Stream API)」の追加です。ストリームAPIはラムダ式をベースにして設計されたもので、これを使うことによってパイプラインや並列処理を容易かつスマートに実装することができます。

 今回は、このストリームAPIの基本的な使い方や並列処理の実装方法などを説明します。また、ストリームAPIには、曖昧な理解のまま使うと誤った結果を返したり、想定したパフォーマンスが得られなかったりするような落とし穴があります。そのような陥りやすい問題点についても、コードを例示しながら解決方法を紹介します。

自動で電話をかける「Robocallシステム」を題材にして考える

 今回の記事でも、前回の記事で使った「Robocallシステム」を題材にして説明していきましょう。

 Robocallシステムは、連絡先のリストの中から、特定の条件にマッチする人に自動的に電話をかけるシステムです。連絡先などの個人情報はクラスPersonのオブジェクトとして管理し、Personオブジェクトの情報を基に条件を満たすか否かを判断して電話をかける処理をメソッドrobocallMatchingPersonsとして実装します。このメソッドの引数としては、条件をテストするメソッドtestを持った関数型インタフェースPersonPredicateのオブジェクトを渡します。

【リスト1:連絡先などの個人情報を保持するクラスPerson】

class Person { int getAge(); Sex getSex(); PhoneNumber getPhoneNumber(); EmailAddr getEmailAddr(); PostalAddr getPostalAddr(); …略… } enum Sex { FEMALE, MALE }

【リスト2:Personオブジェクトの情報を基に発信条件を判定するプログラム
(クラスMyApplicationとインタフェースPersonPredicate)】

class MyApplication { List<Person> list = …略… void robocallMatchingPersons(PersonPredicate pred) { for (Person p : list) { if (pred.test(p)) { PhoneNumber num = p.getPhoneNumber(); robocall(num); } } } } interface PersonPredicate { boolean test(Person p); }

 メソッドrobocallMatchingPersonsの呼び出しは、Java SE 8ではラムダ式を使って次のように書くことができます。メソッドrobocallSelectiveServiceCandidatesは「18歳から25歳までの男性」という条件を、メソッドrobocallEligibleDriversは「16歳以上(性別問わず)」という条件を指定してメソッドrobocallMatchingPersonsを呼び出します。

【リスト3:条件を指定してメソッドrobocallMatchingPersonsを
呼び出すメソッドの実装例
(メソッドrobocallSelectiveServiceCandidatesとrobocallEligibleDrivers)】

void robocallSelectiveServiceCandidates() { robocallMatchingPersons( p -> p.getSex() == MALE && p.getAge() >= 18 && p.getAge() <= 25); } void robocallEligibleDrivers() { robocallMatchingPersons(p -> p.getAge() >= 16); }

Robocallにメッセージ送信機能を追加する

 それでは、このシステムに「携帯電話の番号に対しては、電話をかける代わりにテキスト・メッセージを送信する」という機能を追加してみましょう。リスト2のメソッドrobocallMatchingPersonsと同じ実装からスタートし、それをリファクタリングしていきます。最初の実装例は次のようになります。

【リスト4:携帯電話の番号に対してはテキスト・メッセージを送信するメソッド
txtmsgMatchingPersonsの初期実装】

void txtmsgMatchingPersons(PersonPredicate pred) { for (Person p : list) { if (pred.test(p)) { PhoneNumber num = p.getPhoneNumber(); txtmsg(num); } } }

 メソッドrobocallMatchingPersonsとtxtmsgMatchingPersonsの違いは、条件を満たす人に対して電話をかけるのか、それともメッセージを送るのかという点だけです。そこで、これらのメソッドを共通化し、呼び出し元で条件を満たした場合のアクションを決められるライブラリとして実装してみます。それには、初めに電話番号を受け取って最終的なアクションを実行する処理を関数型インタフェースとして次のように定義します。

【リスト5:電話番号を受け取ってアクションを実行する
関数型インタフェースPhoneNumberConsumer】

interface PhoneNumberConsumer { void accept(PhoneNumber num); }

 Robocallシステム側のライブラリは、このインタフェースを使うと次のように書くことができます。引数としてPersonPredicateオブジェクトに加えてPhoneNumberConsumerオブジェクトを受け取り、robocallやtxtmsgなどのアクションをPhoneNumberConsumerに委譲するという仕組みです。

【リスト6:リスト5のインタフェースを使って実装したメソッド
processMatchingPersons】

void processMatchingPersons(PersonPredicate pred, PhoneNumberConsumer cons) { for (Person p : list) { if (pred.test(p)) { PhoneNumber num = p.getPhoneNumber(); cons.accept(num); } } }

 次に、Personオブジェクトが固定電話と携帯電話の2つの番号を持つ場合を想定して、メソッドgetPhoneNumberは固定電話の番号を返し、携帯電話の番号はメソッドgetMobilePhoneNumberで取得するような仕様を考えてみます。この場合、電話をかけるときとメッセージを送るときでは電話番号の取得に使うメソッドが異なるため、メソッドprocessMatchingPersonsの実装を変更する必要が生じます。そこで、電話番号の取得方法も次のように関数型インタフェース化し、呼び出し側で指定できるようにします。

【リスト7:電話番号の取得方法を指定する
関数型インタフェースPersonToPhoneNumber】

interface PersonToPhoneNumber { PhoneNumber apply(Person p); }

 このインタフェースを使ってメソッドprocessMatchingPersonsを実装すると、次のようになります。

【リスト8:リスト7のインタフェースを使って実装したメソッド
processMatchingPersons】

void processMatchingPersons(PersonPredicate pred, PersonToPhoneNumber p2n, PhoneNumberConsumer cons) { for (Person p : list) { if (pred.test(p)) { PhoneNumber num = p2n.apply(p); cons.accept(num); } } }

 呼び出し側は、3つの引数部分をそれぞれラムダ式化して、次のように書くことができます。関数型インタフェースを使うことにより、メソッドprocessMatchingPersonsの内部で行う条件判定や具体的なアクションを呼び出し側で実装できるようになっているのがポイントです。

【リスト9:リスト8のメソッドを呼び出すメソッド
robocallEligibleDriversとtxtmsgEligibleDriversの
実装例(3つの引数部分をラムダ式化)】

void robocallEligibleDrivers() { processMatchingPersons(p -> p.getAge() >= 16, p -> p.getHomePhoneNumber(), num -> robocall(num)); } void txtmsgEligibleDrivers() { processMatchingPersons(p -> p.getAge() >= 16, p -> p.getMobilePhoneNumber(), num -> txtmsg(num)); }

 さて、ここまでに次の3つの関数型インタフェースを定義しました。

【リスト10:ここまでに定義した3つの関数型インタフェース
PersonPredicate、PhoneNumberConsumer、PersonToPhoneNumber】

interface PersonPredicate { boolean test(Person p); } interface PhoneNumberConsumer { void accept(PhoneNumber num); } interface PersonToPhoneNumber { PhoneNumber apply(Person p); }

 ジェネリクスを使えば、これらをより一般化されたインタフェースとして定義することができます。具体的には、次のようにPersonやPhoneNumberの部分を型パラメータ化するのです。

【リスト11:リスト10の3つのインタフェースをジェネリクスで一般化した例】

@FunctionalInterface interface Predicate<T> { boolean test(T t); } @FunctionalInterface interface Consumer<T> { void accept(T t); } @FunctionalInterface interface Function<T,R> { R apply(T t); }

 @FunctionalInterfaceはJava SE 8で新たに追加されたアノテーションで、このインタフェースが関数型インタフェースであることを示しています。

 このジェネリクス化された関数型インタフェースに合わせてメソッドprocessMatchingPersonsを書き直すと、次のようになります。

【リスト12:リスト11のインタフェースに合わせて書き直したメソッド
processMatchingPersons】

void processMatchingPersons(Predicate<Person> pred, Function<Person,PhoneNumber> p2n, Consumer<PhoneNumber> cons) { for (Person p : list) { if (pred.test(p)) { PhoneNumber num = p2n.apply(p); cons.accept(num); } } }
  • コメント(1件)
#1 勘違いかもしれませんが   2015-08-09 10:41:23
中間操作のための主要なメソッドの説明で
マップとフィルタの内容が入れ替わっているように思います