読者です 読者をやめる 読者になる 読者になる

ぺーぺーSEのブログ

備忘録・メモ用サイト。

Lambda Expression(λ式)のまとめ

Lambda Expression(Lambda式、ラムダ式λ式)についてまとめる(自分用。読み辛いとか知らんw)。
Java8からLambda Expressionを導入するプロジェクト、
Project Lambdaの目的は

  • パラレルに処理するタスクを簡単に記述すること

そのために、取った導入した手段が

  • Functional Interfaceのインスタンス生成を簡単に記述するLambda Expression

だった。以降、

  • Lambda Expression
  • インタフェースのデフォルト実装
  • メソッド参照

について書く。
以降のソースは

で入手した環境で動作確認済み。

>java -version
java version "1.8.0-ea"
Java(TM) SE Runtime Environment (build 1.8.0-ea-lambda-nightly-h109-20130902-b106-b00)
Java HotSpot(TM) Client VM (build 25.0-b45, mixed mode)

Lambda Expression

匿名クラスを簡略化して記述する方法。
関数型インターフェース(後述)を実装した無名クラスのインスタンシエーションの簡易記法。
簡単な処理をするスレッドを起こす時、よくRunnableインターフェースの実装クラスをその場でnewして処理を定義したりする。
そんな時に色々省略して書ける。

記法
(引数, 引数, ...) -> {処理}

言葉で説明するの難しいから例を載せまくる。


■Lambdaを使うとどうなる?1

Runnable task = new Runnable() {
  public void run() {
    System.out.println("Runnable");
  }
}
↓
// Lambda式での記述
// 処理が1つの場合は「{}」を省略可能
Runnable task = () -> System.out.println("Runnable");
// 実行したいときは「task.run();」してね



■Lambdaを使うとどうなる?2

Callable<Integer> not = new Callable<Integer>() {
  public Integer call() {
    System.out.println("Callable_not");
    return 1;
  }
};
↓
// Lambda式での記述
Callable<Integer> task = () -> {
    System.out.println("Callable");
    return 1;
  };
// 実行したいときは「task.call();」してね



■Lambdaを使うとどうなる?3

JButton button = new JButton("OK");
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        // イベント処理
        update();
    }
});
↓
// Lambda式での記述
button.addActionListener( (ActionEvent e) -> update() );
↓
// 型は省略可能
button.addActionListener( (e) -> update() );
↓
// 引数が1つの場合は括弧を省略可能
button.addActionListener( e -> update() );


関数型インターフェース(Functional Interface)

Functional Interface は簡単にいえば 1 つしかメソッドが定義されていないインタフェースのこと。
「@FunctionalInterface」アノテーションが付与されている。(されてないのもいた。。。)


■Functional Interfaceの例

@FunctionalInterface
public interface Runnable {
  public abstract void run();
}
public interface Callable<V> {
  V call() throws Exception;
}
@FunctionalInterface
public interface Iterable<T> {
  Iterator<T> iterator();
  default void forEach(Consumer<? super T> action) {
      Objects.requireNonNull(action);
      for (T t : this) {
          action.accept(t);
      }
  }
  default Spliterator<T> spliterator() {
      return Spliterators.spliteratorUnknownSize(iterator(), 0);
  }
}
public interface ActionListener extends EventListener {
  public void actionPerformed(ActionEvent e);
}



■Functional Interfaceの使用例1

// 独自Functional Interface
@FunctionalInterface
interface Adder {
  Integer add(Integer x, Integer y);
}
↓
Adder adder1 = (Integer x, Integer y) -> { return x + y; };
↓
// 引数の型を省略
Adder adder2 = (x, y) -> { return x + y; };
↓
// 処理が1行であれば波括弧とreturnを省略
Adder adder3 = (x, y) -> x + y;
↓
// 実行 結果は全部3
System.out.println(adder1.add(1, 2));
System.out.println(adder2.add(1, 2));
System.out.println(adder3.add(1, 2));



■Functional Interfaceの使用例2

// 独自Functional Interface
@FunctionalInterface
interface Doubler {
  Integer add(Integer x);
}
↓
Doubler doubler1 = (x) -> x + x;
↓
// 引数が1つであれば、丸括弧も省略可能
Doubler doubler2 = x -> x + x;
↓
// 実行 結果は全部4
System.out.println(doubler1.add(2));
System.out.println(doubler2.add(2));



インタフェースのデフォルト実装

Lambda Expressionの目的は最初に書いたが

  • パラレルに処理するタスクを簡単に記述すること

for文などのループ処理がパラレルに実行できればいいなぁと思う。
しかし、for文などでループを書いても、それをパラレルにすることはできない。
拡張for文で記述するイテレータを外部イテレータという。
この外部イテレータに対して、内部イテレータというものがある。
内部イテレータでは、イテレータをライブラリが制御し、処理の遅延やパラレル化などを行いやすくなるというもの。
Javaで内部イテレータを記述する場合、匿名クラスを使用する。
まずは外部イテレータを使用した例が下記。

class Student {
  public String name;
  public Integer gradYear;
  public Integer score;
  public Student(String name, Integer gradYear, Integer score) {
    this.name = name;
    this.gradYear = gradYear;
    this.score = score;
  }
}

List<Student> students = new ArrayList<>();
students.add(new Student("Taro", 2013, 80));
students.add(new Student("Jiro", 2010, 90));
students.add(new Student("Goro", 2013, 100));
 
int highestScore = 0;

for (Student s: students) {
  if (s.gradYear ==2013) {
    if (s.score > highestScore) {
      highestScore = s.score;
    }
  }
}

これを内部イテレータで記述すると

int highestScore
    = students.stream()
              .filter(new Predicate<Student>() {
                  public boolean test(Student s) {
                      return s.gradYear == 2013;
                  }
              }).map(new Function<Student, Integer>() {
                  public Integer apply(Student s) {
                      return s.score;
                  }
              }).reduce(0, new BinaryOperator<Integer>() {
                  public Integer apply(Integer left, Integer right) {
                      return Math.max(left, right);
                  }
              });

Collectionや配列をまとめて操作できるStreamを使用する。
Streamでは、filterメソッドや、mapメソッドなどを用いて処理を行う。
filterメソッドはCollectionの要素のフィルタリングをするメソッド。引数のPredicateオブジェクトのtestメソッドの戻り値がtrueの要素だけ選択される。
mapメソッドは新しいCollectionを作り直すメソッド。Functionオブジェクトのapplyメソッドの戻り値を新たな要素としたCollectionを作る。
reduceメソッドは要素を減らして、最終的に1つにするためのメソッド。BinaryOperatiorオブジェクトのapplyメソッドの2つの引数の内欲しい方を戻り値とする。
実際にはfilterメソッドもmapメソッドも戻り値はIterableインタフェース。
PredicateインタフェースやFunctionインタフェースはjava.util.functionsパッケージで提供されている。
これをLambda Expressionで書きかえてみる。

int highestScore
        = students.stream()
                  .filter( s -> s.gradYear == 2013 )
                  .map( s -> s.score )
                  .reduce(0, (left, right) -> Math.max(left, right));

実行すると「100」と表示される。
Streamとは、データに対して集約的な処理を行うためのAPIである。
Streamには、オブジェクトに使用するStreamインタフェースと、プリミティブ用のIntStreamインタフェース、DoubleStreamインタフェース等がある。
Streamには、filterメソッドやmapメソッドのようにメソッドの戻り値の型がStreamのメソッドと、forEachメソッドやreduceメソッドのように戻り値がStream以外のメソッドがある。
Streamを使用する場合、filterメソッドなどの戻り値がStreamのメソッドを連ね、最後にそれ以外のメソッドを続けて結果を得る。
このとき、途中の段階の処理は遅延され、最後のメソッドの処理時にまとめて行われる。
パラレル処理版のIteratorインタフェースに相当するインタフェースがSpliteratorインタフェース。
SpliteratorインタフェースはtrySplitメソッドを定義しており、これによって処理を分割していき、最後にまとめて処理を行う。(分割統治)
上記の処理をパラレルに行うにはstreamメソッドをparallelStreamメソッドに変更するだけ。

int highestScore
        = students.parallelStream()
                  .filter( s -> s.gradYear == 2013 )
                  .map( s -> s.score )
                  .reduce(0, (left, right) -> Math.max(left, right));

こういうようにLambda Expressionを使って

  • パラレルに処理するタスクを簡単に記述すること

を実現する。
さて、IterableインターフェースにJava7までは定義されていないfilterメソッドやmapメソッドをどのように付け加えたのだろうか。
Iterableインタフェースにfilterメソッドやmapメソッドを追加してしまうと、インタフェースを実装するクラスはインタフェースで定義したメソッドは必ず定義しなくてはならなくなる。
つまり、昔のソースをコンパイルし直したらインタフェースで定義したメソッドを実装しておらず、コンパイルエラーだらけということになる。
これに対応するために、インタフェースのデフォルト実装がある。
インタフェースで定義したメソッドを実装されていない場合、デフォルト実装が使用される。
例えば、Iterableインタフェースは下記のように記述されている。

@FunctionalInterface
public interface Iterable<T> {

  Iterator<T> iterator();

  default void forEach(Consumer<? super T> action) {
      Objects.requireNonNull(action);
      for (T t : this) {
          action.accept(t);
      }
  }

  default Spliterator<T> spliterator() {
      return Spliterators.spliteratorUnknownSize(iterator(), 0);
  }
}

独自インターフェースでも使用可能。

@FunctionalInterface
interface Hello {
  void sayHello(String name);
  // デフォルトメソッドを付加する
  default void sayGoodbye(String name) {
    System.out.println("Goodbye, " + name + "!");
  }
}

defaultを指定できるメソッドはstaticメソッドだけという制約がある。


メソッド参照 (Method Reference)

関数型ではなくて、あくまでもメソッドに対する参照。
例えば、FileFilterインタフェースで読み取り専用のファイルをフィルタリングする場合、次のように何種類かの書き方がでる。

// 読み取り専用のファイルだけをフィルタリングする
FileFilter filter = new FileFilter() {
    public boolean accept(File f) {
        return f.canRead();
    }
};

FileFilter filter2 = (File f) -> f.canRead();

FileFilter filter3 = f -> f.canRead();

FileFilter filter4 = File::canRead;

最後のメソッド参照を利用した書き方。
引数のメソッドを呼び出すだけのLambda Expressionであれば、メソッドを指定するだけでいい。


Lambda Expressionの勘所を掴むための例

理屈は(だいたい)わかった。
でもサラサラ書けないと書く量が減っても意味がない。(書く量を減らすことが目的ではないが。。。)
以降、勘所を掴めそうな例を残していく。


■Iterable#forEach

class Student {
  public String name;
  public Integer gradYear;
  public Integer score;
  public Student(String name, Integer gradYear, Integer score) {
    this.name = name;
    this.gradYear = gradYear;
    this.score = score;
  }
}

List<Student> students = new ArrayList<>();
students.add(new Student("Taro", 2013, 80));
students.add(new Student("Jiro", 2010, 90));
students.add(new Student("Goro", 2013, 100));

for(Student student: students){
  System.out.println(student.name);
}
↓
students.forEach(new Consumer<Student>() {
  public void accept(Student student) {
    System.out.println(student.name);
  }
});
↓
students.forEach(student -> System.out.println(student.name));



疑問

疑問1

Lambda Expressionの目的は

  • パラレルに処理するタスクを簡単に記述すること

はいいんだが、

  • リアルタイムに処理対象が増えて行って
  • 追加されていく処理対象も随時パラレルにさばいていく

ような仕組みってLambdaでどうやって書くんだ?
Concurrentパッケージ使って明示的に複数スレッド起こすしかないのかな。。。
今のところ固定のCollectionがあって、それをドバっと処理する時しか使えなさそうに見える。。。シングルトンのCollectionにリアルタイムにaddしていったら処理されたりしないのかね?

参考

Java8のStreamを使いこなす
http://d.hatena.ne.jp/nowokay/20130504#1367702641

Java8のStreamの目的と書きやすさや可読性、並行処理の効果について
http://d.hatena.ne.jp/nowokay/20130506#1367793849

Java 8は関数型なのか
http://itpro.nikkeibp.co.jp/article/Watcher/20130712/491204/

ラムダとinvokedynamicの蜜月
http://www.slideshare.net/miyakawataku/lambda-meets-invokedynamic

Project Lambdaの基礎
http://www.slideshare.net/skrb/project-lambda-24531410

Lambdaへの道
http://www.slideshare.net/skrb/lambda-15544054

ラムダ式、JAR脱獄、JavaScript/Node.jsに接近するJDK 8、そして9へ
http://www.atmarkit.co.jp/ait/articles/1204/19/news144.html

Java SE 8 lambdaで変わるプログラミングスタイル
http://d.hatena.ne.jp/nowokay/20131118#1384727782

Java8 Streamではクイックソートが書けない
http://d.hatena.ne.jp/nowokay/20131122#1385104579

直列加算と並列加算でdoubleの足し算の結果が変わる話
http://d.hatena.ne.jp/nowokay/20131123#1385198436

Stream APIの始め方
http://blog.exoego.net/2013/12/kick-start-stream-api.html

Java Advent Calendar 2013 5日目 - Java 8 Stream APIを学ぶ
http://d.hatena.ne.jp/kagamihoge/20131205/1386196696

JavaのLambdaの裏事情
http://www.slideshare.net/nowokay/java-28986016

Java Advent Calendar 9 日目 - Stream のパラレル処理
http://www.javainthebox.com/2013/12/java-advent-calendar-9-stream.html

JJUG CCC 2013 Fallに初登壇してLambdaについて話してきました!
http://d.hatena.ne.jp/bitter_fox/20131111/1384185531

ラムダ禁止について本気出して考えてみた - 9つのパターンで見るStream API
http://acro-engineer.hatenablog.com/entry/2013/12/16/235900

Streamで2つの集計を同時に行う
http://d.hatena.ne.jp/nowokay/20131217#1387256134

Java8には型推論があるので型指定不要で変数が使えますよ
http://d.hatena.ne.jp/nowokay/20131224#1387847065

Java8でラムダで書くかメソッド参照で書くかの指針
http://d.hatena.ne.jp/nowokay/20140109#1389228639