JJUG Night Seminar: Java 8 Lambda 式ハンズオンのまとめ

25 日の JJUG Night Seminar は Java 8 で導入が予定されている Lambda 式のハンズオンでした。内容と所感をまとめておきます。なお、記述内容はハンズオン時の最新ビルド (lambda-8-b48-linux-i586-12_jul_2012) に基づくものです。 Lambda 式の仕様は未確定なので、今後いくらでも変わる可能性があります。

Java 8 の Lambda 式は、抽象メソッドを一つだけ用意しているインタフェースのインスタンスを作るための構文糖です。抽象メソッドを一つだけ用意しているインタフェースとしては、 ComparatorRunnable が挙げられます。処理を匿名クラスで記述するようなものですね。たとえば、 Arrays#sort の呼び出しは、次のように書き換えられます。

Lambda 式の目的は、大きく 2 つです。ひとつは、 Lisp その他の高級言語のような、高階関数渡しによるリスト処理を、 Java で普通に書けるようにすることです (例: numbers.map(x -> x * 2)) 。もうひとつは、 リスト要素に対する細粒度の並列処理を実現可能にすることです (例: numbers.parallel().map(x -> x * 2)) (現時点で parallel メソッドは実装されていない)。

いずれにしても、 Iterable インタフェースに、 map や parallel や forEach のようなメソッドが追加されます。しかし、単にインタフェースを変更すると、既存のコレクションクラスはコンパイルが通らなくなってしまいます。 AbstractCollection とかに実装を追加しとけばいいじゃん、と言いたいところですが、すべてのコレクションクラスが AbstractCollection を継承しているわけではないので、やっぱり駄目です。結局 Java 8 では、インタフェースにメソッドのデフォルト実装が書けるようになります。 Iterable#map や Iterable#parallel はデフォルト実装付きで提供されます。抽象クラスの立場がありませんね。でも便利です。

所感

ベテランのための構文糖としては、ありだと思います。匿名クラスを直に書くよりも、ずっと簡潔だし、コードの意図も分かりやすくなります。

初心者が使うには、いくつかの頻出パターンをイディオムとして覚えてもらうしかないでしょう。あとは、 Arrays#sort みたいなメソッドの Javadoc に、 Lamda 式を使った呼び出しの例を載せてもらうとか、 IDE の補完が素敵に Lambda 式をサジェスチョンしてくれるとか、そうなったらいいな。

そのほか現時点では、いくつか引っ掛かるところがありました。仕様確定までに改善して欲しいところです。

Iterable#reduce の型がおかしい

要素を集計するための Iterable#reduce というメソッドがあります。 Ruby の inject, Scheme の fold に相当します。このメソッドの型がおかしい。

たとえば、従業員の月給の合計を出したい時、 Ruby では次のように書けます。

total_salary = employees.inject(0) { |sum, emp| sum + emp.salary }

Kink ならこうです。

&TOTAL_SALARY = EMPLOYEES.fold(0) { \0 + \1.salary }

Java では次のように書きたいところですが、書けない。

int totalSalary = employees.reduce(0, (sum, e) -> sum + e.getSalary())

なぜ書けないかというと、 Iterable#reduce では、戻り値の型が要素の型 (Employee) と同一でなければならないから。

代わりに、次のように map メソッドと reduce メソッドを組み合わせるか、 mapReduce メソッドを呼ぶ必要があります。

// map + reduce
Iterable<Integer> salaries = employees.map(e -> e.getSalary());
int totalSalary = salaries.reduce(0, (int sum, int sal) -> sum + sal);

// mapReduce
int totalSalary = employees.mapReduce(e -> e.getSalary(), 0, (int sum, int sal) -> sum + sal)

Lambda 式の問題と言うよりも、 API 設計の問題ですね。

型推論がこなれていない

型エラーで結構はまりました。たとえば次のコードは通らない。

int sumSalary = employees.mapReduce(e -> e.getSalary(), 0, (sum, sal) -> sum + sal);

次のコードは通る。

int sumSalary = employees.mapReduce(e -> e.getSalary(), 0, (int sum, int sal) -> sum + sal);

型推論がこなれていないらしい。