Java Magazineロゴ
Java Magazine2013年11月号からの転載。講読申込み受付中。

Java 8:ラムダ式、パート2

Ted Neward著

ラムダ式を有利に使用する方法

Java SE 8のリリースがいよいよ近づいています。Java SE 8では、新言語機能であるラムダ式(別名:クロージャ匿名メソッド)やそれを支援する言語機能とともに、APIおよびライブラリの強化機能も提供される予定で、従来のJavaコア・ライブラリの一部がより使いやすくなります。これらの強化機能や追加機能の多くはCollections APIに対するものであり、Collections APIはアプリケーションの至る所で使用されるため、当然のことながら本記事でも多くの部分で取り上げています。

しかし、ラムダ式の背後にある考え方や、ラムダ式を組み込んだ設計の外観と動作は、ほとんどのJava開発者にとって馴染みのないものであると思われます。したがって、最終段階のラムダ式が披露される前に、なぜラムダ式の設計が今のような外観になっているかをよく知っておくことが得策です。したがって、本記事では前後比較のアプローチを使用して、ラムダ式の適用前と適用後の問題対処方法を確認します。

注:本記事は、Java SE 8のb92ビルド(2013年5月30日版)を使用して執筆されており、本記事の発行時点またはJava SE 8のリリース時点までに、APIや構文、セマンティクスが変更される可能性があります。ただし、APIの背後にある考え方やオラクルのエンジニアが採用したアプローチは、本記事で紹介している内容に近いものとなります。

コレクションとアルゴリズム

注目に値する
関数型寄りの方法でコレクションを操作するアルゴリズムはCollections APIの初期リリースよりAPIに組み込まれていましたが、その使いやすさにもかかわらず注目されることはほとんどありません。

Collections APIはJDK 1.2から存在しますが、そのすべての機能が開発者コミュニティから同様に注目や好感を持たれたわけではありません。関数型寄りの方法でコレクションを操作するアルゴリズムはCollections APIの初期リリースよりAPIに組み込まれていましたが、その使いやすさにもかかわらず注目されることはほとんどありません。たとえば、Collectionsクラスには、コレクションをパラメータとして受け取り、そのコレクションやコレクションの内容に対して何らかの操作を実行することを目的としたメソッドが十数個あります。 

リスト1のPersonクラスを例として考えます。このPersonクラスは、リスト2のように、10個程度のPersonオブジェクトを保持するListによって使用されます。

リスト1

public class Person {
  public Person(String fn, String ln, int a) {
    this.firstName = fn; this.lastName = ln; this.age = a;
  }

  public String getFirstName() { return firstName; }
  public String getLastName() { return lastName; }
        public int getAge() { return age; }
} 


リスト2
 

List<Person> people = Arrays.asList(
      new Person("Ted", "Neward", 42),
      new Person("Charlotte", "Neward", 39),
      new Person("Michael", "Neward", 19),
      new Person("Matthew", "Neward", 13),
      new Person("Neal", "Ford", 45),
      new Person("Candy", "Ford", 39),
      new Person("Jeff", "Brown", 43),
      new Person("Betsy", "Brown", 39)
    );
} 


ここで、このリストの内容を調査し、姓、年齢の順でソートすることにします。すぐに思い付く方法は、forループを記述することです(すなわち、ソートする必要があるたびに、ソートを手作業で実装します)。言うまでもなく、このループによる実装方法には、DRY(Don’t Repeat Yourself、「重複の排除」原則)違反の問題があります。さらに悪いことに、forループは再利用できないため、ループを毎回実装し直す必要もあります。

Collections APIには、もっと優れた方法があります。それは、Collectionsクラスに備わる、Listの内容をソートするsortメソッドを使用するやり方です。ただし、sortメソッドを使用するためには、PersonクラスでComparableインタフェースのメソッド(「自然順序付け」と呼ばれ、すべてのPerson型のデフォルト順序を定義するメソッド)を実装するか、Personオブジェクトのソート方法を定義したComparatorインスタンスをsortメソッドに渡す必要があります。

したがって、まず姓でソートし、姓が同じ場合はさらに年齢でソートする場合のコードは、リスト3のようになります。しかし、姓、年齢の順でソートするという単純な処理のわりには、リスト3のコード量は多すぎます。この煩雑なソート処理こそ、新しいクロージャ機能の力を発揮できるところです。クロージャを使用することでComparatorの記述が容易になります(リスト4)。

リスト3 

 Collections.sort(people, new Comparator<Person>() {
      public int compare(Person lhs, Person rhs) {
        if (lhs.getLastName().equals(rhs.getLastName())) {
          return lhs.getAge() - rhs.getAge();
        }
        else
          return lhs.getLastName().compareTo(rhs.getLastName());
      }
    }); 


リスト4
 

Collections.sort(people, (lhs, rhs) -> {
      if (lhs.getLastName().equals(rhs.getLastName()))
        return lhs.getAge() - rhs.getAge();
      else
        return lhs.getLastName().compareTo(rhs.getLastName());
    }); 


ComparatorはJava言語においてラムダ式の必要性を示す格好の事例であり、1回限りの匿名メソッドが有効となる多数のケースの1つです(この点はおそらくラムダ式のもっとも分かりやすいメリットですが、もっとも弱いメリットであることも心に留めておいてください。基本的には、ある構文をより簡潔な別の構文に置き換えているだけです。しかし、この時点で本記事を読み終えたとしても、この簡潔さだけで大量のコードの削減につながるでしょう)。

今後も長期的にこの比較操作を使用する場合は、いつでもこのラムダ式をComparatorインスタンスとして取得できます。Comparatorインスタンスはラムダ式が適しているメソッドのシグネチャ(この例では「int compare(Person, Person)」)であり、PersonクラスにComparatorインスタンスを直接保持することでラムダ式の実装が容易になり(リスト5)利用箇所の読みやすさも大幅に向上するからです(リスト6)。

リスト5 

public class Person {
  // ...

  public static final Comparator<Person> BY_LAST_AND_AGE =
    (lhs, rhs) -> {
      if (lhs.lastName.equals(rhs.lastName))
        return lhs.age - rhs.age;
      else
        return lhs.lastName.compareTo(rhs.lastName);
    };
} 


リスト6
 

 Collections.sort(people, Person.BY_LAST_AND_AGE); 

匿名の利用
ComparatorはJava言語においてラムダ式の必要性を示す格好の事例であり、1回限りの匿名メソッドが有効となる多数のケースの1つです。


ただ、PersonクラスにComparator<Person>インスタンスを保管する方法は一風変わっています。比較を実行するメソッドを定義して、Comparatorインスタンスの代わりにそのメソッドを使用する方が分かりやすいでしょう。幸いにも、JavaではComparatorのメソッドと同じシグネチャとなる任意のメソッドを使用できます。そのため、BY_LAST_AND_AGE Comparatorを標準的なインスタンスとして記述することも、Personの静的メソッドを記述して(リスト7)代わりに使用することも(リスト8)同様に可能です。

リスト7 

  public static int compareLastAndAge(Person lhs, Person rhs) {
    if (lhs.lastName.equals(rhs.lastName))
      return lhs.age - rhs.age;
    else
      return lhs.lastName.compareTo(rhs.lastName);
  } 


リスト8
 

Collections.sort(people, Person::compareLastAndAge); 


これまで述べたように、Collections APIへの変更部分を除いても、ラムダ式はなお便利で有効な機能であることが分かります。この時点で本記事を読み終えても、十分満足の行く内容でしょう。しかし、さらに多くのことが改善されようとしています。

Collections APIの変更点

Collection関連のクラス自体にもAPIが追加されていますが、より強力な新しいアプローチや技法の数々が開発され、その多くが関数型プログラミングの世界から流用された技法を活用しています。新しいアプローチや技法を使用するために関数型プログラミングの知識は不要です。ただしそれは、関数もクラスやオブジェクトと同様に操作や再利用には重要であるという考え方を受け入れる場合に限ります。

比較:前述のComparatorを使用した方法には、Comparator実装の内部に弱点が潜んでいます。このコードでは2度の比較を行っていますが、1度目の比較は2度目の比較よりも「優先」されます。つまり、姓を最初に比較し、姓が同じ場合のみ年齢を比較します。しかし、後のプロジェクト要件として、最初に年齢でソートし、次に姓でソートすることが必要になった場合、新しいComparatorを記述する必要があり、compareLastAndAgeのいかなる部分も再利用は不可能です。

この部分は、より関数的なアプローチを採用することで強力なメリットが得られます。たとえば、前述の2度の比較を完全に別個のComparatorインスタンスとして見た場合、これらのインスタンスを組み合わせることで、必要な種類の比較操作を作り出すことができます(リスト9)。

リスト9 

public static final Comparator<Person> BY_FIRST =
    (lhs, rhs) -> lhs.firstName.compareTo(rhs.firstName);
  public static final Comparator<Person> BY_LAST =
    (lhs, rhs) -> lhs.lastName.compareTo(rhs.lastName);
  public static final Comparator<Person> BY_AGE =
    (lhs, rhs) -> lhs.age – rhs.age; 


これまで、そのような比較の組合せを手作業で記述することは、あまり生産的ではないとされてきました。そのような比較の組合せを実行するコードを記述する時間は、手作業で多段階の比較コードを記述する時間よりも長くかかるとは言わないまでも、ほぼ同じ程度の時間がかかるからです。

実際、「2つのXを、それぞれのXが持つメソッドから返される値を比較することで比較する」という方法は非常に一般的であり、この機能はJavaプラットフォームに標準で提供されます。Comparatorクラスのcomparingメソッドは、オブジェクトから比較キーを抽出する1つの関数(ラムダ式)を引数として受け取り、そのキーを基準としてソートするComparatorを返します。つまり、リスト9は、リスト10のように、さらに簡単に書き直すことができます。

リスト10 

 public static final Comparator<Person> BY_FIRST =
    Comparators.comparing(Person::getFirstName);
  public static final Comparator<Person> BY_LAST =
    Comparators.comparing(Person::getLastName);
  public static final Comparator<Person> BY_AGE =
    Comparators.comparing(Person::getAge); 

リデュースの利用
標準的に組み込まれたメソッドを使用する場合、新しいJava APIの強力な機能の1つであるリデュース(値のコレクションを結合して、何らかのカスタム操作を通じて1つの値にまとめること)について、詳しく調査する絶好の機会を逃してしまいます。


リスト10の実行内容を少し考えてみます。Personは単純にソートの実行基準となるキーを抽出しているだけで、ソートは行っていません。この変更はすばらしいことです。Personでソートの方法について考える必要がなくなり、PersonPerson自体のことだけに焦点を絞れば良いのですから。

もっとも、特に2つ以上の値を基準として比較を行う場合には、さらに改善できます。

合成:Java 8以降、Comparatorインタフェースには、複数のComparatorインスタンスを連結することで多様な組合せを作成するための、複数のメソッドが含まれます。たとえば、Comparator.thenComparing()メソッドは、最初の比較が完了した後、次の比較に使用するComparatorを引数として受け取ります。したがって、「姓、年齢の順」による比較操作を作り直すと、2つのComparatorインスタンス(LASTAGE)としてコードを記述できます(リスト11)。また、Comparatorインスタンスではなくメソッドを使用したい場合は、リスト12のコードを使用します。

リスト11 

 Collections.sort(people, Person.BY_LAST.
                                   .thenComparing(Person.BY_AGE)); 


リスト12
 

Collections.sort(people,
      Comparators.comparing(Person::getLastName)
                 .thenComparing(Person::getAge)); 


ところで、Collections.sort()に慣れていない開発者の方は、Listsort()メソッドを直接使用することもできます。Listのsort()メソッドは、インタフェースのデフォルト・メソッドの導入による利点の1つです。かつては、この種の継承を使用しない再利用可能な振る舞いについては静的メソッド内に配置する必要がありました。しかし、Java 8より、そのような振る舞いをインタフェース内に配置できるようになりました(詳細は、このシリーズの前回の記事を参照)。

同様に、Personオブジェクトのコレクションを姓、名の順にソートする必要がある場合に、新しいComparatorを記述する必要はありません。この比較も、リスト13のように専用の2つの原子的な比較操作で構成できるからです。

リスト13 

    Collections.sort(people,
      Comparators.comparing(Person::getLastName)
      .thenComparing(Person::getFirstName)); 


このように組合せ可能なメソッドの「連結」は関数合成と呼ばれ、関数型プログラミングにおいては一般的です。また、関数型プログラミングが現在のように強力である理由の核心でもあります。

関数合成の真のメリットは、比較を実行するためのAPIにとどまらず、実行可能コードの一部を渡して(その後それらのコードをこれまでにない画期的な方法で組み合わせて)再利用や設計のチャンスを作り出せることにあります。Comparatorは氷山の一角に過ぎません。特に組み合わせて合成した場合に、多くの操作をより柔軟で強力なものにすることが可能です。

反復:ラムダ式や関数型アプローチによりコーディング方法がどのように変わるかを示す別の例として、反復について考えます。反復は、コレクションを使用して実行する基本的な操作の1つです。Java 8では、IteratorインタフェースとIterableインタフェースに定義されたforEach()デフォルト・メソッドによって、コレクションに変化がもたらされます。たとえば、このデフォルト・メソッドを使用してコレクション内の各項目の内容を出力するためには、IteratorforEachメソッドにラムダ式を渡す必要があります(リスト14)。

リスト14 

people.forEach((it) -> System.out.println("Person:" + it)); 


正式には、渡されるラムダ式の型は、java.util.functionパッケージに定義されたConsumerインスタンスです。ただし、従来のJavaインタフェースとは異なり、Consumerは新しい関数型インタフェースの1つです。つまり、直接実装することはほとんどなく、新しい考え方として、acceptという1つの重要なメソッドの観点でのみ使用します。このacceptは、ラムダ式が提供するメソッドです。それ以外(composeandThenなど)は、この重要なメソッドの観点から定義されたユーティリティ・メソッドであり、この重要なメソッドを支援するように設計されています。

たとえば、andThen()は、1つ目のインスタンスが最初に呼び出され、2つ目のインスタンスがその直後に呼び出されるように2つのConsumerインスタンスを連結して、1つのConsumerに変換します。この変換は便利な合成技法ですが、本記事の対象範囲からは少し外れます。

Collectorの利用
分かりづらい場合は、修正しましょう。標準的に組み込まれたCollectorインタフェースとそのパートナーのCollectorsを使用すれば、コードはより簡単に記述できます。Collectorsは、この種の可変リデュース操作を専門的に扱います。

コレクションの内容を調べるユースケースの多くは、特定の条件を満たす項目を見つけることを目的としています。たとえば、自動化されたコード・システムで、コレクション内のすべての人にビールを贈る必要があるため、コレクション内で飲酒可能な年齢のPersonオブジェクトを判定するといった目的です。このように「複数の項目から成るグループに属する1つの項目に対して作用する」ことは、単純に1つのコレクションを操作することよりも、実際の処理がはるかに広範囲に及びます。ファイル内の各行や、ある結果セットの各行、乱数ジェネレータにより生成された各値に操作を行うことを想像すれば分かるでしょう。Java SE 8では、複数の項目に対する操作の考え方を、コレクションという考え方から一歩先に進めて一般化し、Streamという独自のインタフェース内に持ち込みました。

ストリーム:JDK内の他のインタフェースと同様に、Streamインタフェースも、Collections APIを含むさまざまなシナリオでの使用を想定した基本的なインタフェースです。Streamインタフェースはオブジェクトのストリームを表しており、表面上は、Iteratorと同様に、コレクション内部のオブジェクトに1つずつアクセスします。

しかし、コレクションとは異なり、Streamでは、オブジェクトのコレクションが有限でない場合があります。そのため、ファイルから文字列を取得する操作などの、さまざまな種類のオンデマンド操作に使用できる選択肢となります。関数合成が可能であるのに加えて、「内部的に」並列化を実行できるように、ストリームが設計されているからです。

コード内で21歳未満のPersonオブジェクトをすべて除外する必要がある前述の要件について考えてみます。この場合、Collectionが、Collectionインタフェースに定義されたstream()メソッドを通じてStreamに変換された後は、filterメソッドを使用して、フィルタ後のオブジェクトのみが入力される新しいStreamを生成できます(リスト15)。

リスト15 

people
      .stream()
      .filter(it -> it.getAge() >= 21) 


filterのパラメータはPredicateです。Predicateは、汎用のパラメータを受け取り、Booleanを返すように定義されたインタフェースです。Predicateの目的は、パラメータ・オブジェクトが結果セットの一部に含まれるかを判定することです。

filter()の戻り値は別のStreamになります。そのため、フィルタ後のStreamもその後の操作のために使用できます。たとえば、Streamによって入力される要素のそれぞれにforEach()を使用してアクセスできます。例では、結果の表示を行っています(リスト16)。

リスト16 

 people.stream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) -> 
        System.out.println("Have a beer, " + it.getFirstName())); 


これは、ストリームが合成可能であることを端的に示しています。ストリームを取得して、複数の原子的操作(それぞれの操作はストリームに対して1つの処理のみを実行)を通じてストリームを実行できるのです。さらに、filter()が遅延実行型であること、つまり必要なときにのみオンデマンドでフィルタが実行されるということも重要です。この遅延実行は、Collections APIで慣れ親しんだ、Personオブジェクトのコレクション全体の内容を確認して事前にフィルタしておく実行方法とは異なります。

フィルタ条件:filter()メソッドの引数が1つのPredicateのみであることは、最初は奇妙に思えるかもしれません。仮に、21歳以上かつ姓がNewardであるすべてのPersonオブジェクトを見つけることが目的の場合、filter()Predicateインスタンスを2つ受け取ることができる、もしくは受け取る必要があるように感じるのではないでしょうか。しかし、このように複数の引数を受け取れば、あらゆる可能性が詰まったパンドラの箱を開けることになります。たとえば、22歳以上65歳未満で、かつ名が4文字以上のPersonオブジェクトを見つけることを目的とした場合はどうなるでしょうか。突如として無限の可能性が広がるため、filter() APIですべての可能性に何とかして対応する必要が出てきます。

ただし、言うまでもないことですが、これらすべての可能性を何とかして1つのPredicateに結合するメカニズムが提供されれば、このような対応は必要ありません。幸いにも、Predicateインスタンスのあらゆる組合せを1つのPredicateとして表現できることは容易に想像できます。たとえば、条件Aがtrueかつ条件Bがtrueとなる必要のあるフィルタが存在し、これらの条件が満たされるオブジェクトをフィルタ後のストリームに格納できるとします。その場合、このフィルタ自体がPredicate (A and B)となり、これらの2つのフィルタ条件を組み合わせて1つのPredicateにまとめることができます。そのためには、任意の2つのPredicateインスタンスを引数として受け取り、ABのそれぞれがtrueの場合に限りtrueを返すようなPredicateを記述します。

この「and」で連結したPredicateは、呼び出す必要のある2つのPredicateインスタンスについてのみ知っている(これらのインスタンスのそれぞれに渡されるパラメータについては知らない)ため、完全に汎用的であり、あらかじめ記述しておくことができます。

PredicateクロージャがPredicate参照に格納されている場合(前述のComparator参照をPersonのメンバーとして使用した状況に似ています)、参照のand()メソッドを使用して、参照同士を連結できます(リスト17)。

リスト17 

 Predicate<Person> drinkingAge = (it) -> it.getAge() >= 21;
    Predicate<Person> brown = (it) -> it.getLastName().equals("Brown");
    people.stream()
      .filter(drinkingAge.and(brown))
      .forEach((it) ->

                System.out.println("Have a beer, " +
                                   it.getFirstName()));

 
ご想像のとおり、and()or()xor()というメソッドをすべて使用できます。使用できるすべての機能に関する完全な説明については、Javadocを確認してください。

map()とreduce():よく使用するStreamの操作には、他にmap()があります。map()は、Stream内に存在する各要素に対して関数を適用し、各要素から結果を生成する操作です。たとえば、コレクション内のそれぞれのPersonの年齢を、それぞれのPersonから年齢を取得する単純な関数を適用することで取得できます(リスト18)。

リスト18

  IntStream ages =
      people.stream()
            .mapToInt((it) -> it.getAge()); 


実用性の観点で、IntStream(および親類のLongStreamDoubleStream)が存在します。これらのストリームは、それぞれのプリミティブ型に対応するようにStream<T>インタフェースを特殊化した(すなわち、そのインタフェースのカスタム・バージョンを作成した)ものです。

IntStreamは、PersonインスタンスのCollectionから、整数のStreamを生成します。このような操作は、変換操作と呼ばれることもあります。コードではPersonintに変換または射影されるからです。

同様に、reduce()は、値のストリームを取得し、何らかの操作を通じて、1つの値にリデュースする(まとめる)操作のことです。リデュースは開発者にとってはすでに馴染みのある操作ですが、はじめのうちは認識できないかもしれません。SQLのCOUNT()演算子はリデュース操作の一種で、行のコレクションから1つの整数にリデュースしています。SUM()演算子、MAX()演算子、MIN()演算子も同様です。これらのタスクはそれぞれ、値(行)のストリームを受け取って、ストリーム内の各値に何らかの操作(カウンタのインクリメント、小計への値の加算、最大値の選択、最小値の選択など)を適用して、1つの値(整数)を生成します。

そのため、たとえば値を合計した後にストリーム内の要素数で除算することで、平均年齢を取得できます。新しいAPIを使用すれば、標準的に組み込まれたメソッドを使用するだけで非常に容易にリデュースを実行できます(リスト19)。

リスト19

int sum = people.stream()
                .mapToInt(Person::getAge)
                .sum(); 


ただし、標準的に組み込まれたメソッドを使用する場合、新しいJava APIの強力な機能の1つであるリデュース(値のコレクションを結合して、何らかのカスタム操作を通じて1つの値にまとめること)について、詳しく調査する絶好の機会を逃してしまいます。そこで、新しいreduce()メソッドを使用して、合計の算出部分を書き直してみましょう。 

.reduce(0, (l, r) -> l + r); 


このリデュース操作は、関数型プログラミングの世界では畳み込み(fold)とも呼ばれ、シード値(この例では「0」)から始まり、そのシード値とストリームの最初の要素にクロージャを適用します。その後、結果を取得して累積値として保管します。この累積値は、ストリーム内の次の要素のシード値として使用されます。

たとえば、1、2、3、4、5という整数のリストがある場合、シード値の0が1に加算され、結果(1)が累積値として保管されます。次に、この累積値1が左側の値となり、ストリームの次の数値に加算されます(1+2)。この結果(3)は累積値として保管され、次の加算(3+3)に使用されます。この結果(6)が保管され、次の加算(6+4)に使用され、その結果が最後の加算(10+5)に使用されて、最終結果の15が導き出されます。実際にリスト20のコードを実行した場合、この15という結果が得られます。

リスト20 

List<Integer> values = Arrays.asList(1, 2, 3, 4, 5);
    int sum = values.stream().reduce(0, (l,r) -> l+r);
    System.out.println(sum); 


ここで、reduceの第2引数として受け取ったクロージャの型はIntBinaryOperatorである点に注意してください。このIntBinaryOperatorは2つの整数を引数として受け取り、intの結果を返すものとして定義されています。IntBinaryOperatorIntBiFunctionは特殊化された関数型インタフェースの一例であり、2つのパラメータ(型は同一の場合も異なる場合もあります)を受け取り、intを返します。その他にも、DoubleLongに対応する特殊化バージョンもあります。これらの特殊化バージョンは、おもに一般的なプリミティブ型を使用するために必要となるコーディング作業を軽減する目的で作成されました。

IntStreamには、average()メソッド、min()メソッド、max()メソッドなどの、比較的よく使用される整数操作を実行するヘルパー・メソッドもいくつか用意されています。また、バイナリ操作(2つの数値の合計など)の多くも、対応する型のプリミティブ・ラッパー・クラスに定義されています(Integer::sumLong::maxなど)。

その他のマップ操作やリデュース操作:マップ操作やリデュース操作は、単純な計算以外にもさまざまな状況で活用できます。結局のところ、オブジェクトのコレクションを別の1つのオブジェクト(または値)に変換し、その後1つの値にまとめることができるあらゆるケースで、マップ操作やリデュース操作は機能します。

たとえば、マップ操作は、オブジェクトを引数として受け取り、そのオブジェクトの一部分を抽出するような抽出や射影の操作に使用できます。たとえば、次のようにPersonオブジェクトから姓を抽出できます。 

Stream lastNames =   people.stream()      .map(Person::getLastName);  


Personストリームから姓を取得した後は、リデュース操作によって文字列を連結できます。このようなリデュース操作は、姓をXMLデータ表現に変換するような場合に使用できます(リスト21)。

リスト21 

String xml =
      "<people data='lastname'>" +
      people.stream()
            .map(it -> "<person>" + it.getLastName() + "</person>")
            .reduce("", String::concat)
      + "</people>";
    System.out.println(xml); 


当然ながら、異なるXML形式が要求される場合には、異なる操作を使用してそれぞれの形式の内容を制御できます。この際に、リスト21のように形式を都度指定することも、リスト22のようにPersonクラス自体に定義するなどして別のクラスに定義されたメソッドを使用することもできます。別のクラスに定義されたメソッドをmap()操作の一部に使用することで、Personオブジェクトのストリームを、オブジェクト要素から成るJSON配列に変換できます(リスト23)。

リスト22

public class Person {
  // ...
  public static String toJSON(Person p) {
    return
      "{" +
        "firstName:\"" + p.firstName + "\", " +
        "lastName:\"" + p.lastName + "\", " +
        "age:" + p.age + " " +
      "}";
  }
} 


リスト23
 

String json =
      people.stream()
        .map(Person::toJSON)
        .reduce("[", (l, r) -> l + (l.equals("[") ?"" :",") + r)
        + "]";
    System.out.println(json); 


準備を整える
Java SE 8のリリースが急速に近づいています。Java SE 8では、新言語機能であるラムダ式(別名:クロージャ、匿名メソッド)やそれを支援する言語機能とともに、APIおよびライブラリの強化機能も提供される予定で、従来のJavaコア・ライブラリの一部がより使いやすくなります。

リスト23のreduce操作内にある三項演算子は、最初のPersonの前にあるカンマがJSONへシリアライズされることを防ぐためにあります。カンマが前に存在する形式を許容するJSONパーサーもありますが、そのことは保証されないため、見た目は悪いですが三項演算子を使用しています。

 

実際に分かりづらい場合は、修正しましょう。標準的に組み込まれたCollectorインタフェースとそのパートナーのCollectorsを使用すれば、このコードはより簡単に記述できます。Collectorsは、この種の可変リデュース操作を専門的に扱います(リスト24)。その結果、以前の明示的なreduceString::concatを使用した例よりも、コーディングにかかる時間を大幅に短縮できるというメリットがあります。したがって、Collectorsを使用する方が一般的には良いと言えます。

リスト24

  String joined = people.stream()
                          .map(Person::toJSON)
                          .collect(Collectors.joining(", "));System.out.println("[" + joined + "]"); 


ところで、話の発端のComparatorを忘れてしまわないように付け加えると、Streamには、ストリームを一時的にソートするための操作もあります。Personリストのソート後のJSON表現を取得する処理はリスト25のようになります。

リスト25

String json = people.stream()
                        .sorted(Person.BY_LAST)
                        .collect(Collectors.joining(", " "[", "]"));
    System.out.println(json); 


このソート操作は非常に便利です。

並列化:さらに強力な機能があります。それは、Stream経由で抽出した各オブジェクトに対する処理に必要となるロジックから、前述の操作がすべて完全に分離されていることです。つまり、大規模なコレクションに対して反復、マップ、あるいはリデュースの操作を実行するときに、コレクションを複数のセグメントに分割して、各セグメントを異なるスレッドで処理することによって、従来のforループを細かく分割できます。

Learn More


 ラムダ式

ただし、Stream APIはすでにこの並列化に対応しているものの、前述のXMLやJSONへのmap()操作とreduce()操作については、少し変更が必要になります。コレクションからStreamを取得するためにstream()を呼び出している部分で、代わりにparallelStream()を使用します(リスト26)。

リスト26

  people.parallelStream()
      .filter((it) -> it.getAge() >= 21)
      .forEach((it) ->
                System.out.println("Have a beer " + it.getFirstName() +
                  Thread.currentThread())); 


10個以上の項目を含むコレクションの場合、少なくとも筆者のラップトップでは、2つのスレッドを使用してコレクションが処理されます。それは、mainという名前のスレッド(Javaクラスのmain()メソッドを呼び出すために使用される従来のスレッド)と、ForkJoinPool.commonPool worker-1という名前のスレッド(明らかに開発者が作成したものではないスレッド)です。

10個の項目を含むコレクションの場合は明らかに複数のスレッドは不要ですが、数百個の項目ともなれば、「十分」な状態から「高速化が必要」な状態に変化します。これらの新しいメソッドやアプローチがなければ、開発者は大量のコードとアルゴリズムの調査に目を凝らすことになるでしょう。しかし、新しいメソッドやアプローチを使用することで、前述の順次処理に対して、文字通り8回(streamのsを大文字にするために必要なShiftキーも入れれば9回)のキーストロークを加えるだけでコードを並列化できます。

さらに、多くの読者の推測どおり、必要であればsequential()というメソッドを呼び出すことで、並列化されたStreamを順次ストリームに戻すこともできます。

重要なのは、順次処理と並列処理のいずれが適しているかにかかわらず、両方に対して同じStreamインタフェースを使用できることです。順次的な実装も並列的な実装も、すべて実装の詳細になり、ビジネス・ニーズ(およびビジネス価値)に集中したコードを扱う場合に理想的です。スレッド・プール内のスレッドの起動や複数のスレッドの同期など、低レベルの詳細は扱いたくないからです。

まとめ

ラムダ式は、Javaコードの記述方法と設計方法の両方に多くの変化をもたらします。これらの変化の一部は、Java SEライブラリにおいてすでに起こっています。また、開発者がラムダ式の機能(と欠点)に慣れ親しむにつれて、その他多くのライブラリ(Javaプラットフォームが所有するライブラリ、オープンソースという広大なライブラリの両方)の中でゆっくりとその変化が実現されていくことでしょう。

Java SE 8リリースにはラムダ式の他にも多数の変更が加えられています。しかし、コレクションに対するラムダ式の動作を理解すれば、ラムダ式を独自の設計やコード内で活用する方法について考える際の大きな強みとなり、今後数年の間、より分離の進んだコードを作成できるようになります。


Ted Neward@tedneward):Neudesicのアーキテクチャ・コンサルタント。複数の専門家グループに貢献。Effective Enterprise Java(Addison-Wesley Professional、2004年)、Professional F# 2.0(Wrox、2010年)などの著書多数。Java、Scalaをはじめとするテクノロジー記事の掲載やカンファレンスでの講演も多い。