Your SlideShare is downloading. ×
社内Java8勉強会 ラムダ式とストリームAPI
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×

Saving this for later?

Get the SlideShare app to save on your phone or tablet. Read anywhere, anytime - even offline.

Text the download link to your phone

Standard text messaging rates apply

社内Java8勉強会 ラムダ式とストリームAPI

30,342
views

Published on


0 Comments
64 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
30,342
On Slideshare
0
From Embeds
0
Number of Embeds
30
Actions
Shares
0
Downloads
138
Comments
0
Likes
64
Embeds 0
No embeds

Report content
Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
No notes for slide

Transcript

  • 1. 1 / 54 Java8基礎勉強会 ラムダ式とストリームAPI 2014年3月25日 アリエル・ネットワーク 池添
  • 2. 2 / 54 本日のテーマ
  • 3. 3 / 54 for文を駆逐してやる! この世から1つ残らず!
  • 4. 4 / 54 目次 • 概要 • ラムダ式の基礎 • ストリームAPIの基礎 • ストリームAPIの拡張
  • 5. 5 / 54 ラムダ式とストリームAPI • ラムダ式とは関数を簡便に表現するための記法。 • ストリームAPIは、ラムダ式を利用したコレク ション操作用のAPI • 関数型プログラミング言語由来。歴史は古い。 • これまでの手続き型やオブジェクト指向的なプ ログラミング手法から、関数型プログラミング に変わります。 • パラダイムシフトのよかん!!
  • 6. 6 / 54 簡単なサンプル • フルーツの一覧の中から • 名前が“りんご”で始まり、 • 値段が100円以上のものを、 • 値段順で並び替え、 • 名前だけを取り出して、 • リストを作成する 1 List<String> apples = fruits.stream() 2 .filter(f -> f.getName().startsWith("りんご")) 3 .filter(f -> f.getPrice() > 100) 4 .sorted(Comparator.comparingInt(Fruit::getPrice)) 5 .map(Fruit::getName) 6 .collect(Collectors.toList());
  • 7. 7 / 54 メリット • 手続き的だった記述が宣言的になる • 保守性の向上…? • 可読性の向上…? • 簡単に並列実行できるようになる
  • 8. 8 / 54 保守性が向上する? データの取 得 データの取 得 条件による 抽出 条件による 抽出 データの加 工 データの加 工 コンテナへ の登録 コンテナへ の登録 for文for文 デー タの 初期 化 デー タの 取得 条件による抽出条件による抽出 データの加工 コンテナへの 登録 \ ごちゃっ / パイプライン的ですっきり • 処理の追加・削除・順番の入れ替えなどがやり やすい。 • 再利用性やテスタビリティも向上するかも。
  • 9. 9 / 54 可読性は? • 野球選手の一覧から、チームごとの投手の平均 年俸を取得する処理の例。 1 Map<String, Double> m = players.stream() 2 .collect(Collectors.groupingBy(player -> player.getTeam())) 3 .entrySet() 4 .stream() 5 .collect(Collectors.toMap( 6 entry -> entry.getKey(), 7 entry -> entry.getValue().stream() 8 .filter(player -> player.getPosition().equals("投手")) 9 .mapToInt(player -> player.getSalary()) 10 .average() 11 .orElse(0) 12 ) 13 ); • 気をつけないとすぐに読みにくくなる。 • stream()とかstream()とかstream()とかノイズが多い。
  • 10. 10 / 54 ラムダ式の基礎
  • 11. 11 / 54 むかしばなし Javaでdelegate型を扱えるようにしたで! J++って言いますねん。 互換性のないものはJavaとは呼べません。 提訴します! なんとまあセンスの悪い言語設計でしょう。 まともな言語設計者ならポリシーが 許さないと思います。 じゃあ、自分らで言語つくりますわ。 C#って言うやつ。 1997年 1998年 2000年 delegateをラムダ式で書けるようにしたで。 2005年 Microsoft Sun 某研究者
  • 12. 12 / 54 なぜラムダ式が必要になったのか • 非同期処理や並列処理が当たり前に使われるよ うになり、ラムダ式の必要性が高まった。 • Microsoftの提案を受け入れていれば、ラムダ 式がもっと早く入っていたかもしれない。 • でも、15年も先を見越して言語設計するなん てことは難しい。
  • 13. 13 / 54 ラムダ式 • 関数を第一級オブジェクトとして扱えるように なった。 • JVMで関数を直接扱えるようになったわけでは なく、内部的にはクラスのインスタンスを使っ て表現している。
  • 14. 14 / 54 ラムダ式の書き方 () -> 123 x -> x * x (x, y) -> x + y (int x, int y) -> { return x + y; } いろいろ省略できる! 仮引数や戻り値の型はほとんど 型推論してくれる。かしこい!
  • 15. 15 / 54 ラムダ式の使い方 • ラムダ式を渡される側 void hoge(Function<Integer, Integer> func) { Integer ret = func.apply(42); } • ラムダ式を渡す側 hoge(x -> x * x); 関数型インタフェース
  • 16. 16 / 54 関数型インタフェース • ラムダ式の型は関数型インタフェースで表現さ れる。 • 関数型インタフェースとは • 実装するべきメソッドを1つだけ持ってる interface • @FunctionalInterfaceアノテーションを付ける と、コンパイル時にメソッドが1つだけかどうか チェックしてくれる。つけなくてもよい。 @FunctionalInterface public interface Function<T, R> { R apply(T t); }
  • 17. 17 / 54 標準の関数型インタフェース • Runnable, Consumer, Function, Predicate,Supplier • BiConsumer,BiFunction,BiPredicate • BooleanSupplier • IntBinaryOperator,IntConsumer,IntFunction,IntPre dicate,IntSupplier,IntToDoubleFunction,IntToLong Function,IntUnaryOperator,ObjIntConsumer,ToIntBi Function,ToIntFunction • LongBinaryOperator,LongConsumer,LongFunction,Lon gPredicate,LongSupplier,LongToDoubleFunction,Lon gToIntFunction,LongUnaryOperator,ObjLongConsumer ,ToLongBiFunction,ToLongFunction • DoubleBinaryOperator,DoubleConsumer,DoubleFuncti on,DoublePredicate,DoubleSupplier,DoubleToIntFun ction,DoubleToLongFunction,DoubleUnaryOperator,O bjDoubleConsumer,ToDoubleBiFunction,ToDoubleFunc tion
  • 18. 18 / 54 代表的な関数型インタフェース • Runnable: 引数なし、戻り値なし • Consumer: 引数1つ、戻り値なし • Function: 引数1つ、戻り値あり • Predicate: 引数1つ、戻り値がboolean • Supplier: 引数なし、戻り値あり • Bi + Xxxxx: 引数が2つ
  • 19. 19 / 54 無名内部クラスとラムダ式の違い 無名内部クラス ラムダ式 外部変数へのアクセス 実質的なfinalの変 数のみアクセス可能。 実質的なfinalの変数のみ アクセス可能。 エンクロージングイン スタンスの参照 必ず持っている。 必要がなければ持たない。 メモリリークがおきにくい。 クラスファイルの生成 コンパイル時にクラ スが生成される。 実行時にクラスが生成され る。 クラスのロード時間が短縮 されるかも。 インスタンスの生成 明示的にnewする。 JVMが最適な生成方法を選 択する。
  • 20. 20 / 54 Javaのラムダ式はクロージャではない • ローカル変数は、実質的にfinalな変数にしか アクセスできない。 • 独自のスコープは持たず、外のスコープを引き 継ぐ。 • エンクロージングインスタンスの参照は、基本 持たない。
  • 21. 21 / 54 ラムダ式のスコープ class Outer { public void func() { final int a = 0; int b = 1; list.stream().forEach(x -> { int a = 2; System.out.println(b); }); b = 3; } } 値の変更が行われるローカル 変数はアクセス不可 独自のスコープを持たな いので、変数名の衝突が 起きる。 明示しなければ、エンクロー ジングインスタンスの参照を 持たない。
  • 22. 22 / 54 例外は苦手かも • ラムダ式からチェック例外のあるAPIを呼び出す 場合 • 関数型インタフェースの定義にthrowsを記述する (標準の関数型インタフェースにはついてない) • ラムダ式の中でtry-catchを書く list.map(x -> { try { // チェック例外のある呼び出し } catch(XxxException ex) { // エラー処理。 } }).collect(Collectors.toList());
  • 23. 23 / 54 メソッド参照 • ラムダ式だけでなく、既存のメソッドも関数型 インタフェースで受け取ることが可能。 // ラムダ式を使った場合 list.forEach(x -> System.out.println(x)); // メソッド参照を使った場合 list.forEach(System.out::println); fruits.map(fruit -> fruit.getName()); // インスタンスメソッドの参照もOK fruits.map(Fruit::getName);
  • 24. 24 / 54 Lambda Expression Deep Dive • ラムダ式は無名内部クラスのシンタックスシュガーじゃな い。 • コンパイル時ではなく実行時にクラスが生成される。 • invokeDynamic命令を使っている。 • ラムダ式をコンパイルするとどんなバイトコードが生成さ れるかみてみよう。 対象のコードはこんな感じ public class Main { public void main(){ Sample sample = new Sample(); sample.func(x -> x * x); } }
  • 25. 25 / 54 ラムダ式のバイトコード INVOKEDYNAMIC apply()Ljava/util/function/IntFunction; [ // handle kind 0x6 : INVOKESTATIC java/lang/invoke/LambdaMetafactory.metafactory() // arguments: (I)Ljava/lang/Object;.class, // handle kind 0x6 : INVOKESTATIC Main.lambda$main$0((I)Ljava/lang/Integer;) , (I)Ljava/lang/Integer;.class ] BIPUSH 12 INVOKEVIRTUAL Sample.func (Ljava/util/function/IntFunction;I)I // ・・・途中省略・・・ private static lambda$main$0(I)Ljava/lang/Integer; L0 // ラムダ式の中の処理 x -> x * x ラムダのオブジェクトをスタックに積んで メソッドを呼び出す ラムダ式の オブジェクト をつくる命令 ラムダ式の なかみ
  • 26. 26 / 54 ラムダ式の実行時の挙動 Main.classMain.class コンパイル時に 生成されるもの invoke dynamicinvoke dynamic static method lambda$main$0 static method lambda$main$0 LambdaMetafactory #metafactory LambdaMetafactory #metafactory ラムダのインス タンスをつくる メソッド ラムダのインス タンスをつくる メソッド class Lambda$1 関数型インタフェー スを実装 lambda$main$0 を呼び出す class Lambda$1 関数型インタフェー スを実装 lambda$main$0 を呼び出す JVMの中のクラス 実行時に作られるもの 1回目の呼び出し (ブートストラップ) 2回目以降 の呼び出し (Method Handle) 作成 Mainの内部クラス として作成 new
  • 27. 27 / 54 なぜこんな複雑なことを? • コンパイル時にクラスを大量につくると、クラ スロードのときに遅くなるかもしれないから。 とくにストリームAPIではラムダ式を大量につ かうので。 • invokeDynamicを使うとパフォーマンスをあ まり落とさず動的な処理が実現できる。 • 今後JVMの実装が変わると、インスタンスの生 成方法がより効率的なものになるかも。
  • 28. 28 / 54 ストリームAPIの基礎
  • 29. 29 / 54 ストリームAPIとは • パイプライン型のデータ処理のAPI • 絞り込み、データ変換、グループ化、集計など の操作をそれぞれ分離した形で記述できる。 • 絞り込みの条件や、加工方法などをラムダ式で 指定する。 • メソッドチェイン形式で記述できる。
  • 30. 30 / 54 ストリームパイプライン • ストリームパイプラインの構成要素 • ソース(Source) • 中間操作(Intermediate Operation) • 終端操作(Terminal Operation) • ストリームパイプラインは、1つ以上のソース、 0個以上の中間操作、1つの終端操作から構成 される。 • 1つのStreamに対して複数の終端操作(いわゆ る分配)をおこなってはいけない。 • 終端操作の結果をソースとして処理を継続する ことも可能。
  • 31. 31 / 54 サンプル • 1行目がソース • 2行目から5行目までが中間操作 • 6行目が終端操作 1 List<String> apples = fruits.stream() 2 .filter(f -> f.getName().startsWith("りんご")) 3 .filter(f -> f.getPrice() > 100) 4 .sorted(Comparator.comparingInt(Fruit::getPrice)) 5 .map(Fruit::getName) 6 .collect(Collectors.toList());
  • 32. 32 / 54 ストリームパイプラインの挙動 中間 操作 中間 操作 中間 操作 結果 filterの条件に 一致しなければ 以降の処理は実 行しない ソースの要素を 1つずつ中間操作 に流す 終端操作を呼び出したときに 初めて全体の処理が動く。 それまで中間操作は実行され ない。 ソース 終端操作 を実行
  • 33. 33 / 54 ソース • 既存のデータからStream型のオブジェクトを つくる。 • Streamの種類 • Stream<T> • IntStream, LongStream, DoubleStream • つくりかた • Collection#stream • Arrays#stream • Stream#of • BufferReader#lines • IntStream#range
  • 34. 34 / 54 ソースの特性 • Sequential, Parallel • 逐次実行か、並列実行か。 • Stream#parallelとStream#sequentialで相互に 変換可能。 • Ordered, Unordered • Listや配列などはOrdered, SetなどはUnordered • Stream#unorderedで順序を保証しないStreamに 変換可能。 • 無限リスト
  • 35. 35 / 54 中間操作 • 絞り込みや写像などの操作を指定して、新しい Streamを返す。 • 遅延実行 • 処理方法を指定するだけで、実際には処理しない。 • 中間操作を呼び出すたびにループしてたら効率が悪い。 終端操作が呼ばれたときに、複数の中間操作をまとめて ループ処理。 • 処理の種類 • 絞り込み: filter • 写像: map, flatMap • 並び替え: sorted • 数の制御: limit, skip • 同一要素除外: distinct • tee的なもの: peek
  • 36. 36 / 54 特殊な中間操作 • ステートフルな中間操作:distinct, sorted • 中間操作は基本的にステートレスだが、ステートフ ルなものもある。 • 無限リストや並列処理のときに注意が必要。 • ショートサーキット評価な中間操作:limit • 指定された数の要素を流したら処理を打ち切る。 • 無限Streamを有限Streamにしてくれる。 • 副作用向け中間操作:peek • 中間操作では基本的に副作用するべきでない。 • デバッグやログ出力用途以外にはなるべく使わない ようにしよう。
  • 37. 37 / 54 終端操作 • ストリームパイプラインを実行して、なんらか の結果を取得する処理。 forEachだけは戻り値を返さない。副作用専用 のメソッド。 • 処理の種類 • たたみ込み:collect, reduce • 集計:min, max, average, sum, count • 単一の値の取得:findFirst, findAny • 条件:allMatch, anyMatch, noneMatch • 繰り返し:forEach, forEachOrdered
  • 38. 38 / 54 汎用的な終端操作:collect • 引数にCollectorを指定して様々な処理がおこなえる。 • 集計: • averagingInt, averagingLong, averagingDouble • summingInt, summingLong, summingDouble • counting • maxBy, minBy • summarizing • グループ化 • groupingBy, partitioningBy • コンテナに累積 • toList, toMap, toSet • 結合 • joining • たたみ込み • reducing
  • 39. 39 / 54 Optionalを返す終端操作 • Optional • nullチェックめんどくさい・・・。パイプラインの途 中でnullが現れると流れが止まってしまう。 • nullを駆逐し(ry • Java8ではOptional型が入った。 • Stream APIの中にはOptionalを返す終端操作 がある。 • min, max, average, sum, findFirst, findAny, reduceなど。 • 空のリストに対してこれらの処理を呼び出すと Optional.emptyを返す。
  • 40. 40 / 54 ショートサーキット評価の終端操作 • ショートサーキット評価をする終端操作 • anyMatch, allMatch, noneMatch, findFirst, findAny • 例えば、Stream#countは全要素をループで回 すので時間がかかるが、Stream#findAnyは ショートサーキット評価なので、1つめの要素 が見つかればすぐに終わる。 if(stream.count() != 0) if(stream.findAny().isPresent())
  • 41. 41 / 54 並列処理 • すごく簡単に並列化できる。ソースを生成するときに、 Collection#parallelStreamや, Stream#parallel を使うだけ。 • 順序保証 • ソースがORDEREDであれば並列実行しても順番は保証される。 • ただし、順序を保つために内部にバッファリングするので、 あまりパフォーマンスはよくない。 • 順番を気にしないのであれば、unorderedすると効率がよく なる。 • 副作用に注意 • 中間操作で副作用が生じる場合はちゃんとロックしよう。 • ただし、ロックすると並列で実行しても待ちが多くなるので、 パフォーマンスはよくない。 • スレッドセーフでないArrayListなども並列実行可能。
  • 42. 42 / 54 並列処理のやりかた • parallel()をつけるだけ! • ただし、上記のような例ではあまり並列処理の うまみはない。 1 List<String> apples = fruits.stream().parallel() 2 .filter(f -> f.getName().startsWith("りんご")) 3 .filter(f -> f.getPrice() > 100) 4 .sorted(Comparator.comparingInt(Fruit::getPrice)) 5 .map(Fruit::getName) 6 .collect(Collectors.toList());
  • 43. 43 / 54 並列処理の挙動 中間 操作 中間 操作 中間 操作 結果 43 分割したソースごとに 異なるスレッドで中間 操作を並列に実行 ソースを複数に分割 spliterator 順序を保証する 場合は内部で バッファリング 中間 操作 中間 操作 中間 操作 複数スレッドの 実行結果を結合終端操作
  • 44. 44 / 54 ストリームAPIの拡張
  • 45. 45 / 54 ストリームAPIは機能不足? • なんかいろいろ足りない! • 他の言語と比べて標準で用意されてる機能が少ない。 • 複数のStreamを合成するzipすらないなんて… • 毎回stream()を呼ぶのめんどくさい… • 足りないならつくればいいじゃない。 • 関数型言語なら関数をどんどん増やせばよい。 C#なら拡張メソッドという仕組みがある。 • でも、JavaではStreamに自由にメソッドを増やせ ない!こまった!
  • 46. 46 / 54 ストリームAPIの拡張ポイント • たたみ込みを使えばたいていの処理はつくれる。 • 汎用的なCollectorをつくって、再利用できる ようにしておくとよさそう。 • CollectorをつくるためのヘルパーAPIが用意 されている。既存のCollectorを組み合わせた り、一から新しい終端操作をつくったりするこ とができる。
  • 47. 47 / 54 Collectorの構成要素 • supplier • コンテナの初期値を生成する人 • accumulator • 値を加工してコンテナに格納する人 • combiner • 並列で実行された場合、コンテナを結合する人 • finisher • 最後にコンテナを加工する人
  • 48. 48 / 54 Collectorの動き コンテナコンテナ コンテナコンテナ コンテナコンテナ 結果結果 supplier supplier combiner finisher accumulator accumulator 並列的にデータがやってくる
  • 49. 49 / 54 Collectorのつくり方 • Collector.of • supplier, accumulator, combiner, finisher を指定して新しいCollectorをつくる • Collectors.mapMerger • Collectorにcombinerを追加する • Collectors.mapping • accumulatorの前に実行される写像処理を追加す る • Collectors.collectingAndThen • Collectorにfinisherを追加する
  • 50. 50 / 54 Collectorを使った例 • チームごとの投手の平均年俸を取得する • 2つのCollectorを用意 • グループ化したストリームを返すCollector • グループごとの平均値を返すCollector 1 Map<String, Double> averageSalaryMap = players.stream() 2 .filter(player -> player.getPosition().equals("投手")) 3 .collect(groupedStreamCollector(Player::getTeam)) 4 .collect(averagePerGroupCollector(Player::getSalary));
  • 51. 51 / 54 Collectorの実装 1 static <T, V> Collector<Entry<V, List<T>>, ?, Map<V, Double>> 2 averagePerGroupCollector(ToIntFunction<T> mapper) { 3 return Collector.of( 4 () -> new HashMap<>(), 5 (map, entry) -> { 6 entry.getValue().stream() 7 .mapToInt(mapper) 8 .average() 9 .ifPresent(ave -> map.put(entry.getKey(), ave)); 10 }, 11 (left, right) -> { 12 left.putAll(right); 13 return left; 14 } 15 ); 16 } 17 static <T, K> Collector<T, ?, Stream<Entry<K, List<T>>>> 18 groupedStreamCollector(Function<T, ? extends K> mapper) { 19 return Collectors.collectingAndThen( 20 Collectors.groupingBy(mapper), map -> map.entrySet().stream()); 21 } accumulator combiner finisher • つくるのは難しいけど、汎用化しておけばいろ いろなところで使える。 supplier
  • 52. 52 / 54 まとめ • ラムダ式 • 関数を簡便に表記するための記法。 • デメリットも少ないし使わない手はないよね。 • ストリームAPI • 慣れるまでは読み書きも難しいし、変なバグを生み やすいかもしれない。 • でも慣れると少ない記述で複雑な処理が実現できる。 そして何より書いていて楽しい! • しかし、標準で用意されてる機能が少ないのに、拡 張が難しいのはどうにかならんかね? • 今後はオレオレコレクションやStreamUtilsクラ スが乱立する可能性も!?
  • 53. 53 / 54 おまけ • Java8を使うときはIntelliJ IDEAがおすすめ。 (EclipseのJava8正式対応は2014年6月) • Java8の文法にもしっかり対応(たまーに型推 論間違えたりするけど) • 無名内部クラスを書いてると、ラムダ式に書き 換えてくれる。 • for文を書いてると、ストリームAPIに書き換 えてくれる。(複雑な処理はムリだけど)
  • 54. 54 / 54 参考 • JavaのLambdaの裏事情 • http://www.slideshare.net/nowokay/java-2898601 • ラムダと invokedynamic の蜜月 • http://www.slideshare.net/miyakawataku/lambda-meetsinvokedynamic • 倭マン's BLOG • http://waman.hatenablog.com/category/Java8 • ラムダ禁止について本気出して考えてみた - 9つのパターンで見る Stream API • http://acro-engineer.hatenablog.com/entry/2013/12/16/235900 • Collectorを征す者はStream APIを征す(部分的に) • http://blog.exoego.net/2013/12/control-collector-to-rule-stream- api.html • きしだのはてな • http://d.hatena.ne.jp/nowokay/searchdiary?word=%2A%5Bjava8%5D • 徹底解説!Project Lambdaのすべて returns • http://www.slideshare.net/bitter_fox/java8-launchJava • SE 8 lambdaで変わるプログラミングスタイル • http://pt.slideshare.net/nowokay/lambdajava-se-8-lambda