エンジニア的なネタを毎週書くブログ

東京でWebサービスの開発をしています 【英語版やってみました】http://taichiw-e.hatenablog.com/

Java8 Lamda式 改めて。: JavaOne2013 レポート4 #j1jp

Venkat Subramaniam氏による、「Programing with Lambda Expressions in Java」のレポートです。

本セッションはライブコーディング形式で、従来のコードがどのようにLambda式に置き換わっていくのかを解説していました。
既に知っている人にとっては特に目新しいものではなかったと思うのですが、ステップバイステップで話が進んだので、非常にわかりやすかったです。
また、このレポートではうまく伝えることができないのですが、Subramaniam氏の話術が非常に面白く、引き込まれるプレゼンテーションでした。

最初に総括

JavaOne初日のキーノートでも触れられていましたが、Lambda式のメリットは、マルチコアを効率的に使って速く処理する ということにのみならず、従来に比べてコレクション関連のコードが非常にシンプルに、わかりやすく書けることが特徴です。

本セッションでも、その点にフォーカスが置かれて説明がなされていました。

なお、本セッション内で紹介されていたサンプルコードはすべて、以下からダウンロード可能です。こちらのレポートでも、公開されているサンプルコードを抜粋して紹介させていただきます。
http://www.agiledeveloper.com/downloads.html

Lambda式基礎の基礎

最初のお題として、1~6までの数値が入ったListの値を順に出力するプログラムを考えてみます。
一番ベタベタな書き方をするとこんな感じ。

import java.util.*;
import java.util.function.*;

public class Sample {
  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    for(int i = 0; i < numbers.size(); i++) {
      System.out.println(i);
    }
}

これで、「123456」(縦に)の出力が得られます。

ただまぁ、『いまどきこんな書き方するやついないけどな!』(氏の発言の意訳)なので、Java5以降であれば、こういう風にループを書くのが普通でしょう。

    for(int e : numbers) {
      System.out.println(e);
    }

で、こういう書き方をExternal Iterator(外部イテレータ)って言うそうなんですが、こいつをInternal Iterator(内部イテレータ)的な書き方にするとこうなります。

    numbers.forEach(new Consumer<Integer>() {
      public void accept(Integer number) {
        System.out.println(number);
      }
    });

ぱっと見わかりにくくなっちゃってますが、繰り返し内部の処理を別クラスに切り出せたことで、繰り返し内部の処理を隠蔽することができました。

さて、ここからがJava8の新機能、Lambda式の登場です。Lambda式は、上記でConsumerクラスとして定義していた処理を、非常に簡潔に記述することができます。

numbers.forEach((Integer number) -> System.out.println(number));

『How Sweet!!』めちゃくちゃシンプルになりました。
一つ前のコードと比較すると、acceptメソッドが一行で記述されたことになります。

一般的に、メソッド(というか手続き型プログラムの関数)は、「引数」「本体」「返り値」の定義からなります。acceptメソッドはvoid型なので返り値は無しですね。
そのうち、引数がLambda式の左辺、本体が右辺に記述されています。
クラス名、メソッド名は不要なのでありません。無名クラス、無名メソッドです。

もう少しシンプルにします。numbersがIntegerのリストですから、要素であるnumberはわざわざ書かなくてもIntegerに決まっています。ということで無駄なので、Integerは取ってしまいます。

numbers.forEach(number -> System.out.println(number));

ずいぶんすっきりしましたが、Lambda式の両辺に"number"が残っていますね。
『Javaプログラマは2回も同じことを書くなんてアホなことはしないんだぜ! IDEは勝手にしやがるけどな』
ということで、最後に残った重複も取り去ってしまって、これが最終形です。
改めてクラス全体を書くとこうなりました。

import java.util.*;
import java.util.function.*;

public class Sample {
  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    numbers.forEach(System.out::println);
  }
}

::はメソッドへの参照を表す記号で、本来forEachの引数はLambda式なのですが、単一メソッドのみの場合は、このように書けるとのことでした。

比較を入れてみる

今度はリストの各値をそれぞれ2倍して、その合計値を出してみます。
まずは伝統的な書き方から。

import java.util.*;
import java.util.function.*;

public class Sample {
  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

    int totalOfValuesDoubled = 0;
    for(int number : numbers) {
      totalOfValuesDoubled += number * 2;
    }
    System.out.println(totalOfValuesDoubled);
}

『こいつは汚ぇ!! 子供が見たらエップバリって言うぜぇぇ!!!』
(元の表現は "It's dirty! Kids say "Don't touch me!" みたいな感じ)

で、streamっていうfancy iteratorを使ってこう書くのがJava8式。

import java.util.*;
import java.util.function.*;

public class Sample {
  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
  
    System.out.println(
      numbers.stream()
      .mapToInt(number -> number * 2)
      .sum());
  }
}

map()メソッドはコレクションの各要素に同じ処理を施すメソッドで、ここではmapToIntなので、各要素を2倍する ということになります。

上から順に読んでいくと、
1. これから処理する結果を表示する
2. numbersコレクションに対して順に処理する
3. 各要素を2倍する
4. 3.の結果を合計する
となります。元の書式に比べるとかなりシンプルになっています。

 

今度は、filterメソッドを使ってみます。
お題は、「1~6を順に見て行って、3より大きい偶数で最初に出てきたものを2倍して表示」というもの。
(なので、答えは4の2倍の8です)

今度はいきなりLambda式を使いますが、このようになります。

import java.util.*;
import java.util.function.Predicate;

  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    //double the first even number greater than 3 from the list
    
    System.out.println(
      numbers.stream()
      .filter(number -> number > 3)
      .filter(number -> number % 2)
      .mapToInt(number -> number * 2)
      .findFirst()
      .getAsInt()
    );
  }
}

これでも十分読みやすいのですが、各Lambda式を、適切な名前のついたメソッドに置き換えることで、さらに「読める」プログラムにすることができます。

import java.util.*;
import java.util.function.Predicate;

public class Sample {
  public static boolean isGreaterThan3(int number) {
    return number > 3;
  }
 
  public static boolean isEven(int number) {
    return number % 2 == 0;
  }
 
  public static int doubleIt(int number) {
    return number * 2;
  }
 
  public static void main(String args) {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
    //double the first even number greater than 3 from the list
    
    System.out.println(
      numbers.stream()
      .filter(Sample::isGreaterThan3)
      .filter(Sample::isEven)
      .mapToInt(Sample::doubleIt)
      .findFirst()
      .getAsInt()
    );
  }
}

※大体この手のメソッドって碌な名前がつかないので(特に日本人がつけると)、個人的にはメソッドかしないほうが読みやすそうに見えますが。

パラレル処理が威力を発揮するとき

最後に、parallelStreamの速さの実験。YahooFinanceが提供している株価APIを連続でコールして、最も株価が高い銘柄を求めるというものでした。
通信が絡むため繰り返しによるオーバーヘッドが大きい処理です。
これをparallelStreamを使って並列化することでめっちゃ速くなる!という例。

以前はこういった処理を記述するためにはマルチスレッドで意図的に書く必要がありましたが、今後は非常にシンプルに書くことができるようになるようです。