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 + "!"); } }
メソッド参照 (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