Java SE 8のラムダ式の基礎──なぜ必要なのか? 従来記法のリファクタリングを通して、その本質を理解する

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

冗長性を排除する──ラムダ式の導入

 さて、私たちは関数型インタフェースと匿名内部クラスという仕組みにより、大きな一歩を踏み出しました。「値をパラメータ化する」という最初の段階から、関数型インタフェースと匿名内部クラスを使って「振る舞いをパラメータ化する」という次の段階に進んだのです。関数型インタフェースにより、ライブラリを使う発信者側は、必要なときに振る舞いを実装した匿名内部クラスを作ることが可能になったのです。

 この仕組みは、ライブラリ側にとっては最高です。なぜなら、コードがシンプルになるからです。ただし一方で、ライブラリを使う発信者側にとっては少々面倒です。なぜなら毎回、振る舞いを実装した匿名内部クラスを作らなければならないからです。しかも、コーディングの量が随分と増えてしまいました。ここで、改めてリスト11、12のコードをご覧ください。これらのコードを見て、Javaの文法を満たすためだけの冗長な部分が多く含まれているということに気づかれるでしょう。

 例えば、「16歳以上」という条件を実装したリスト12のコードの場合、本当の意味で必要なのは「Personを入力とし、16歳以上か否かを判定して、結果をboolean値で返す」という部分であり、ほかのコードはアルゴリズムの本質とはまったく関係がありません。

 そこで、この本質部分だけを書けば目的を達せられるようにしたのが、Java SE 8の新機能であるラムダ式です。「16歳以上」や「18歳以上25歳以下の男性」という条件に対するメソッドrobocallMatchingPersonsの呼び出しをラムダ式で書き直すと、次のようになります。

【リスト13:ラムダ式で書き直したメソッドrobocallEligibleDrivers、robocallSelectiveService】

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

 上記のコード中、「p」がパラメータ(引数)で、メソッドrobocallEligibleDriversなら「p.getAge() >= 16」が処理本文(ボディ)であり、それをアロー演算子(->)でつないでいます。これがJava SE 8におけるラムダ式の書き方です。メソッドrobocallEligibleDriversを例にとると、下図に示すような具合に簡略化されています。

 なお、このとき呼び出し先の実装には一切手を加える必要がないというのもラムダ式の特徴です。呼び出し先では、パラメータとして匿名内部クラスが使われたのか、ラムダ式が使われたのかを意識する必要はないのです。

 では、ラムダ式の書き方について少し補足しておきましょう。

 まず上述したように、ラムダ式ではアロー演算子の左に引数を書き、右側にボディを書きます。次の式では2つの引数a、bを取り、それらを足し合わせています。

(int a, int b) -> a + b

 次に示すのは引数が1つの例です。この式では、引数aに1を足し合わせています。

(int a) -> a + 1

 引数がない場合は、次のように空の括弧を書きます。

() -> 42

 ラムダ式では、文(ステートメント)を書くこともできます。その場合は、文を{と}の波括弧で挟みます。また、ステートメント型のラムダ式が値を返す場合はreturn文を使います。次に示すのは、上のラムダ式をステートメント型で表現した例です。

(int a, int b) -> { println(a + b); return a + b; }
(int a) -> { println(a + 1); return a + 1; }
() -> { println("Returning 42."); return 42; }

 さらに、コンパイラが型推論機構によって引数の型を推論できる場合には、次に示すように引数の型を省略することができます。

(a, b) -> a + b
(a) -> a + 1
() -> 42
(a, b) -> { println(a + b); return a + b; }
(a) -> { println(a + 1); return a + 1; }
() -> { println("Returning 42."); return 42; }

 加えて、引数が1つの場合には、次に示すように()を省略することもできます。

(a, b) -> a + b
a -> a + 1
() -> 42
(a, b) -> { println(a + b); return a + b; }
a -> { println(a + 1); return a + 1; }
() -> { println("Returning 42."); return 42; }

ラムダ式とデフォルト・メソッド

 続いて、Java SE 8で追加されたもう1つの新機能である「デフォルト・メソッド」について説明します。デフォルト・メソッドとは、インタフェースのメソッドに対して、それがオーバーライドされなかった場合のデフォルトの挙動を持たせることができる機能です。

 デフォルト・メソッドの導入は、ラムダ式の導入と密接に関係しています。ラムダ式により、今後Javaプログラムの書き方は大きく変わるでしょう。それは既存のJavaライブラリについても同様で、ラムダ式を使って最適化できる部分は無数にあるはずです。

 しかし、インタフェースを変更してしまうと、過去のコードとの互換性が失われてしまいます。例えば、インタフェースListに、ラムダ式に対応した新たなメソッドを追加したとしましょう。ご存じのとおり、インタフェースを実装(implements)したクラスを作る場合は、そのインタフェースで宣言されたすべてのメソッドをオーバーライドしなければなりません。つまり、もしListに新しいメソッドを追加したら、Listをimplementsしている既存のコードをすべて書き直さなければならないという事態になるのです。

 この問題を回避するために導入されたのがデフォルト・メソッドです。インタフェース内に定義されたメソッドにデフォルトの実装を持たせられるようになったことで、「すべてのメソッドをオーバーライドしなければならない」というインタフェース実装の制限が緩和されました。その結果、新たなメソッドを追加しても、既存のコードとの互換性が保たれるようになったわけです。

 デフォルト・メソッドを定義するには、メソッドの宣言にdefault修飾子を付加するだけです。次に示すのは、インタフェースMapに対してgetOrDefaultというデフォルト実装を持ったメソッドを定義した例です。

【リスト14:デフォルト・メソッドを定義したインタフェースMap】

interface Map<K, V> { V get(Object key); V put(K key, V value); // …略… default V getOrDefault(Object key, V defaultValue) { V v = get(key); if ((v != null) || containsKey(key)) { return v; } else { return defValue; } } }

ラムダ式を使ったJavaライブラリの拡張

 Java SE 8では、既存のクラスにもラムダ式を利用するさまざまなメソッドが追加されています。例を挙げると、次のようなものがあります。これらは、いずれもデフォルトの実装を持っており、過去のコードとの互換性が保たれています。

 例えば、インタフェースIterableのメソッドforEachはイテレータの各要素に対して順番に指定した処理を実行するというメソッドで、次のように使用します。

【リスト15:インタフェースIterableのメソッドforEachを使うコード】

List<Person) list = …; list.forEach(p -> System.out.println(p));

 メソッドforEachと同じ処理は、拡張for文でも簡単に記述することができます。しかし、例えばListオブジェクトがクラスCollectionsのメソッドsynchronizedListによって同期化されたものだった場合、その内部の挙動は大きく異なってきます。拡張for文ではロックの獲得と解放がループの回数分だけ行われるのに対して、メソッドforEachでは1回だけで済むのです。データ量が多い場合、この違いは大きく影響してくるでしょう。

 以上、ここでは従来のスタイルで書いたコードをリファクタリングしながらラムダ式の意義を説明し、併せて導入されたデフォルト・メソッドも紹介しました。ラムダに関連した新機能としては、デフォルト・メソッドのほかにストリームAPIがあります。次回の記事では、このストリームAPIについて説明します。

>> 後編の記事はこちら

  • コメント(3件)
#1 mau.RunDog   2014-11-22 19:16:28
リスト15の
List<Person) list = …;
は、
List<Person> list = …;
の誤りではありませんか?
#2 ケンジ   2015-04-30 01:56:47
他の言語では当たり前にできていたことを、大々的に新機能って言われてもねぇ。
#3 Koさん   2015-04-30 13:29:28
初心者のために lisp に c++ の皮を被せたものの
Java利用者のレベルが上がったので
皮を一枚剥いだ形でしょうね
このサイトでは、利用状況の把握や広告配信などのために、Cookieなどを使用してアクセスデータを取得・利用しています。 これ以降ページを遷移した場合、Cookieなどの設定や使用に同意したことになります。
Cookieなどの設定や使用の詳細、オプトアウトについては詳細をご覧ください。
[ 閉じる ]