1 / 54
Java8基礎勉強会
ラムダ式とストリームAPI
2014年3月25日
アリエル・ネットワーク 池添
2 / 54
本日のテーマ
3 / 54
for文を駆逐してやる!
この世から1つ残らず!
4 / 54
目次
• 概要
• ラムダ式の基礎
• ストリームAPIの基礎
• ストリームAPIの拡張
5 / 54
ラムダ式とストリームAPI
• ラムダ式とは関数を簡便に表現するための記法。
• ストリームAPIは、ラムダ式を利用したコレク
ション操作用のAPI
• 関数型プログラミング言語由来。歴史は古い。
• これまでの手続き型やオブジェクト指向的なプ
ログラミング手法から、関数型プログラミング
に変わります。
• パラダイムシフトのよかん!!
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 / 54
メリット
• 手続き的だった記述が宣言的になる
• 保守性の向上…?
• 可読性の向上…?
• 簡単に並列実行できるようになる
8 / 54
保守性が向上する?
データの取
得
データの取
得
条件による
抽出
条件による
抽出
データの加
工
データの加
工
コンテナへ
の登録
コンテナへ
の登録
for文for文
デー
タの
初期
化
デー
タの
取得
条件による抽出条件による抽出
データの加工
コンテナへの
登録
\ ごちゃっ / パイプライン的ですっきり
• 処理の追加・削除・順番の入れ替えなどがやり
やすい。
• 再利用性やテスタビリティも向上するかも。
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 / 54
ラムダ式の基礎
11 / 54
むかしばなし
Javaでdelegate型を扱えるようにしたで!
J++って言いますねん。
互換性のないものはJavaとは呼べません。
提訴します!
なんとまあセンスの悪い言語設計でしょう。
まともな言語設計者ならポリシーが
許さないと思います。
じゃあ、自分らで言語つくりますわ。
C#って言うやつ。
1997年
1998年
2000年
delegateをラムダ式で書けるようにしたで。 2005年
Microsoft
Sun
某研究者
12 / 54
なぜラムダ式が必要になったのか
• 非同期処理や並列処理が当たり前に使われるよ
うになり、ラムダ式の必要性が高まった。
• Microsoftの提案を受け入れていれば、ラムダ
式がもっと早く入っていたかもしれない。
• でも、15年も先を見越して言語設計するなん
てことは難しい。
13 / 54
ラムダ式
• 関数を第一級オブジェクトとして扱えるように
なった。
• JVMで関数を直接扱えるようになったわけでは
なく、内部的にはクラスのインスタンスを使っ
て表現している。
14 / 54
ラムダ式の書き方
() -> 123
x -> x * x
(x, y) -> x + y
(int x, int y) -> {
return x + y;
}
いろいろ省略できる!
仮引数や戻り値の型はほとんど
型推論してくれる。かしこい!
15 / 54
ラムダ式の使い方
• ラムダ式を渡される側
void hoge(Function<Integer, Integer> func) {
Integer ret = func.apply(42);
}
• ラムダ式を渡す側
hoge(x -> x * x);
関数型インタフェース
16 / 54
関数型インタフェース
• ラムダ式の型は関数型インタフェースで表現さ
れる。
• 関数型インタフェースとは
• 実装するべきメソッドを1つだけ持ってる
interface
• @FunctionalInterfaceアノテーションを付ける
と、コンパイル時にメソッドが1つだけかどうか
チェックしてくれる。つけなくてもよい。
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
}
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 / 54
代表的な関数型インタフェース
• Runnable: 引数なし、戻り値なし
• Consumer: 引数1つ、戻り値なし
• Function: 引数1つ、戻り値あり
• Predicate: 引数1つ、戻り値がboolean
• Supplier: 引数なし、戻り値あり
• Bi + Xxxxx: 引数が2つ
19 / 54
無名内部クラスとラムダ式の違い
無名内部クラス ラムダ式
外部変数へのアクセス 実質的なfinalの変
数のみアクセス可能。
実質的なfinalの変数のみ
アクセス可能。
エンクロージングイン
スタンスの参照
必ず持っている。 必要がなければ持たない。
メモリリークがおきにくい。
クラスファイルの生成 コンパイル時にクラ
スが生成される。
実行時にクラスが生成され
る。
クラスのロード時間が短縮
されるかも。
インスタンスの生成 明示的にnewする。 JVMが最適な生成方法を選
択する。
20 / 54
Javaのラムダ式はクロージャではない
• ローカル変数は、実質的にfinalな変数にしか
アクセスできない。
• 独自のスコープは持たず、外のスコープを引き
継ぐ。
• エンクロージングインスタンスの参照は、基本
持たない。
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 / 54
例外は苦手かも
• ラムダ式からチェック例外のあるAPIを呼び出す
場合
• 関数型インタフェースの定義にthrowsを記述する
(標準の関数型インタフェースにはついてない)
• ラムダ式の中でtry-catchを書く
list.map(x -> {
try {
// チェック例外のある呼び出し
} catch(XxxException ex) {
// エラー処理。
}
}).collect(Collectors.toList());
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 / 54
Lambda Expression Deep Dive
• ラムダ式は無名内部クラスのシンタックスシュガーじゃな
い。
• コンパイル時ではなく実行時にクラスが生成される。
• invokeDynamic命令を使っている。
• ラムダ式をコンパイルするとどんなバイトコードが生成さ
れるかみてみよう。
対象のコードはこんな感じ
public class Main {
public void main(){
Sample sample = new Sample();
sample.func(x -> x * x);
}
}
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 / 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 / 54
なぜこんな複雑なことを?
• コンパイル時にクラスを大量につくると、クラ
スロードのときに遅くなるかもしれないから。
とくにストリームAPIではラムダ式を大量につ
かうので。
• invokeDynamicを使うとパフォーマンスをあ
まり落とさず動的な処理が実現できる。
• 今後JVMの実装が変わると、インスタンスの生
成方法がより効率的なものになるかも。
28 / 54
ストリームAPIの基礎
29 / 54
ストリームAPIとは
• パイプライン型のデータ処理のAPI
• 絞り込み、データ変換、グループ化、集計など
の操作をそれぞれ分離した形で記述できる。
• 絞り込みの条件や、加工方法などをラムダ式で
指定する。
• メソッドチェイン形式で記述できる。
30 / 54
ストリームパイプライン
• ストリームパイプラインの構成要素
• ソース(Source)
• 中間操作(Intermediate Operation)
• 終端操作(Terminal Operation)
• ストリームパイプラインは、1つ以上のソース、
0個以上の中間操作、1つの終端操作から構成
される。
• 1つのStreamに対して複数の終端操作(いわゆ
る分配)をおこなってはいけない。
• 終端操作の結果をソースとして処理を継続する
ことも可能。
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 / 54
ストリームパイプラインの挙動
中間
操作
中間
操作
中間
操作
結果
filterの条件に
一致しなければ
以降の処理は実
行しない
ソースの要素を
1つずつ中間操作
に流す
終端操作を呼び出したときに
初めて全体の処理が動く。
それまで中間操作は実行され
ない。
ソース
終端操作
を実行
33 / 54
ソース
• 既存のデータからStream型のオブジェクトを
つくる。
• Streamの種類
• Stream<T>
• IntStream, LongStream, DoubleStream
• つくりかた
• Collection#stream
• Arrays#stream
• Stream#of
• BufferReader#lines
• IntStream#range
34 / 54
ソースの特性
• Sequential, Parallel
• 逐次実行か、並列実行か。
• Stream#parallelとStream#sequentialで相互に
変換可能。
• Ordered, Unordered
• Listや配列などはOrdered, SetなどはUnordered
• Stream#unorderedで順序を保証しないStreamに
変換可能。
• 無限リスト
35 / 54
中間操作
• 絞り込みや写像などの操作を指定して、新しい
Streamを返す。
• 遅延実行
• 処理方法を指定するだけで、実際には処理しない。
• 中間操作を呼び出すたびにループしてたら効率が悪い。
終端操作が呼ばれたときに、複数の中間操作をまとめて
ループ処理。
• 処理の種類
• 絞り込み: filter
• 写像: map, flatMap
• 並び替え: sorted
• 数の制御: limit, skip
• 同一要素除外: distinct
• tee的なもの: peek
36 / 54
特殊な中間操作
• ステートフルな中間操作:distinct, sorted
• 中間操作は基本的にステートレスだが、ステートフ
ルなものもある。
• 無限リストや並列処理のときに注意が必要。
• ショートサーキット評価な中間操作:limit
• 指定された数の要素を流したら処理を打ち切る。
• 無限Streamを有限Streamにしてくれる。
• 副作用向け中間操作:peek
• 中間操作では基本的に副作用するべきでない。
• デバッグやログ出力用途以外にはなるべく使わない
ようにしよう。
37 / 54
終端操作
• ストリームパイプラインを実行して、なんらか
の結果を取得する処理。
forEachだけは戻り値を返さない。副作用専用
のメソッド。
• 処理の種類
• たたみ込み:collect, reduce
• 集計:min, max, average, sum, count
• 単一の値の取得:findFirst, findAny
• 条件:allMatch, anyMatch, noneMatch
• 繰り返し:forEach, forEachOrdered
38 / 54
汎用的な終端操作:collect
• 引数にCollectorを指定して様々な処理がおこなえる。
• 集計:
• averagingInt, averagingLong, averagingDouble
• summingInt, summingLong, summingDouble
• counting
• maxBy, minBy
• summarizing
• グループ化
• groupingBy, partitioningBy
• コンテナに累積
• toList, toMap, toSet
• 結合
• joining
• たたみ込み
• reducing
39 / 54
Optionalを返す終端操作
• Optional
• nullチェックめんどくさい・・・。パイプラインの途
中でnullが現れると流れが止まってしまう。
• nullを駆逐し(ry
• Java8ではOptional型が入った。
• Stream APIの中にはOptionalを返す終端操作
がある。
• min, max, average, sum, findFirst,
findAny, reduceなど。
• 空のリストに対してこれらの処理を呼び出すと
Optional.emptyを返す。
40 / 54
ショートサーキット評価の終端操作
• ショートサーキット評価をする終端操作
• anyMatch, allMatch, noneMatch, findFirst,
findAny
• 例えば、Stream#countは全要素をループで回
すので時間がかかるが、Stream#findAnyは
ショートサーキット評価なので、1つめの要素
が見つかればすぐに終わる。
if(stream.count() != 0)
if(stream.findAny().isPresent())
41 / 54
並列処理
• すごく簡単に並列化できる。ソースを生成するときに、
Collection#parallelStreamや, Stream#parallel
を使うだけ。
• 順序保証
• ソースがORDEREDであれば並列実行しても順番は保証される。
• ただし、順序を保つために内部にバッファリングするので、
あまりパフォーマンスはよくない。
• 順番を気にしないのであれば、unorderedすると効率がよく
なる。
• 副作用に注意
• 中間操作で副作用が生じる場合はちゃんとロックしよう。
• ただし、ロックすると並列で実行しても待ちが多くなるので、
パフォーマンスはよくない。
• スレッドセーフでないArrayListなども並列実行可能。
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 / 54
並列処理の挙動
中間
操作
中間
操作
中間
操作
結果 43
分割したソースごとに
異なるスレッドで中間
操作を並列に実行
ソースを複数に分割
spliterator
順序を保証する
場合は内部で
バッファリング
中間
操作
中間
操作
中間
操作
複数スレッドの
実行結果を結合終端操作
44 / 54
ストリームAPIの拡張
45 / 54
ストリームAPIは機能不足?
• なんかいろいろ足りない!
• 他の言語と比べて標準で用意されてる機能が少ない。
• 複数のStreamを合成するzipすらないなんて…
• 毎回stream()を呼ぶのめんどくさい…
• 足りないならつくればいいじゃない。
• 関数型言語なら関数をどんどん増やせばよい。
C#なら拡張メソッドという仕組みがある。
• でも、JavaではStreamに自由にメソッドを増やせ
ない!こまった!
46 / 54
ストリームAPIの拡張ポイント
• たたみ込みを使えばたいていの処理はつくれる。
• 汎用的なCollectorをつくって、再利用できる
ようにしておくとよさそう。
• CollectorをつくるためのヘルパーAPIが用意
されている。既存のCollectorを組み合わせた
り、一から新しい終端操作をつくったりするこ
とができる。
47 / 54
Collectorの構成要素
• supplier
• コンテナの初期値を生成する人
• accumulator
• 値を加工してコンテナに格納する人
• combiner
• 並列で実行された場合、コンテナを結合する人
• finisher
• 最後にコンテナを加工する人
48 / 54
Collectorの動き
コンテナコンテナ コンテナコンテナ
コンテナコンテナ
結果結果
supplier supplier
combiner
finisher
accumulator accumulator
並列的にデータがやってくる
49 / 54
Collectorのつくり方
• Collector.of
• supplier, accumulator, combiner, finisher
を指定して新しいCollectorをつくる
• Collectors.mapMerger
• Collectorにcombinerを追加する
• Collectors.mapping
• accumulatorの前に実行される写像処理を追加す
る
• Collectors.collectingAndThen
• Collectorにfinisherを追加する
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 / 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 / 54
まとめ
• ラムダ式
• 関数を簡便に表記するための記法。
• デメリットも少ないし使わない手はないよね。
• ストリームAPI
• 慣れるまでは読み書きも難しいし、変なバグを生み
やすいかもしれない。
• でも慣れると少ない記述で複雑な処理が実現できる。
そして何より書いていて楽しい!
• しかし、標準で用意されてる機能が少ないのに、拡
張が難しいのはどうにかならんかね?
• 今後はオレオレコレクションやStreamUtilsクラ
スが乱立する可能性も!?
53 / 54
おまけ
• Java8を使うときはIntelliJ IDEAがおすすめ。
(EclipseのJava8正式対応は2014年6月)
• Java8の文法にもしっかり対応(たまーに型推
論間違えたりするけど)
• 無名内部クラスを書いてると、ラムダ式に書き
換えてくれる。
• for文を書いてると、ストリームAPIに書き換
えてくれる。(複雑な処理はムリだけど)
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

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

  • 1.
  • 2.
  • 3.
  • 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.
  • 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.
  • 11.
  • 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 ラムダ式のスコープ classOuter { 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 LambdaExpression Deep Dive • ラムダ式は無名内部クラスのシンタックスシュガーじゃな い。 • コンパイル時ではなく実行時にクラスが生成される。 • invokeDynamic命令を使っている。 • ラムダ式をコンパイルするとどんなバイトコードが生成さ れるかみてみよう。 対象のコードはこんな感じ public class Main { public void main(){ Sample sample = new Sample(); sample.func(x -> x * x); } }
  • 25.
    25 / 54 ラムダ式のバイトコード INVOKEDYNAMICapply()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 コンパイル時に 生成されるもの invokedynamicinvoke 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.
  • 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.
  • 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.
  • 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の実装 1static <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