More Related Content Similar to 社内Java8勉強会 ラムダ式とストリームAPI Similar to 社内Java8勉強会 ラムダ式とストリームAPI (17) More from Akihiro Ikezoe (6) 社内Java8勉強会 ラムダ式とストリームAPI4. 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
メリット
• 手続き的だった記述が宣言的になる
• 保守性の向上…?
• 可読性の向上…?
• 簡単に並列実行できるようになる
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()とかノイズが多い。
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が最適な生成方法を選
択する。
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の実装が変わると、インスタンスの生
成方法がより効率的なものになるかも。
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());
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());
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
• 最後にコンテナを加工する人
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