java8 lambdas chap03
TRANSCRIPT
第3章の内容
• External Iteration と Internal Iteration
• Stream のメソッドの分類: lazy と eager
• Stream のメソッドの使用例: collect , map , filter ,flatMap,
min(max) , reduce
• レガシーコードのリファクタリング例
※本章でも、p3 のDomainクラス (Artist, Track, Album) を使ってサンプルコードが記述されています
2
From External Iteration to Internal Iteration (1)
• (例) ロンドン出身のArtistの数を数える – この処理に対応した3つの例
• Example 3-1 forループ 使用
• Example 3-2 iterator 使用
• Example 3-3 stream 使用
• Example 3-1 forループ 使用
• いくつかの問題
– 多くの boilerplate code
– parallel version を書くのが難しい
– プログラマの意図が伝わりにくい
– ループ の 本体を読む必要 • 本体が長い場合、特にnestedループの場合、負担
int count = 0; for (Artist artist : allArtists) { if (artist.isFrom("London")) { count++; } }
3
From External Iteration to Internal Iteration (2)
• Example 3-2 iterator 使用 -> External Iteration
– 明示的に hasNext と next を呼び出し
• Figure 3-1. External Iteration
• - 異なる振る舞いのoperationの抽象化が難しい
int count = 0; Iterator<Artist> iterator = allArtists.iterator(); while(iterator.hasNext()) { Artist artist = iterator.next(); if (artist.isFrom("London")) { count++; } }
4
From External Iteration to Internal Iteration (3)
• Example 3-3 stream 使用 -> Internal Iteration
• Stream とは – 関数的アプローチで、collection に対して 複雑な operation を構築するツール
• Figure 3-2. Internal Iteration
long count = allArtists.stream() .filter( artist -> artist.isFrom("London") ) .count(); Stream のメソッド
関数 (戻り値: true or false)
5
From External Iteration to Internal Iteration (4)
• Example 3-3 では 2つのシンプルなoperation に分解
– London 出身の全ての artist を見つける
– そのartistのlistの数を数える
→ 2つの operation 共に Stream インタフェースで対応
– filter()
• test に pass した object のみを残す
– test は 関数で定義
» true or false で返す
– count()
• Stream の中の object の数を数える
6
What’s Actually Going On (1)
• Stream のメソッド
– builder pattern に似ている
• プロパティを設定する一連のメソッドの呼び出し
• -> 最後に build メソッドが呼び出されて初めてobjectが作成される
– 多くの異なる operation に対して、1回の iterate で済ます
• Lazy – 例) filter
– 戻り値: Stream • builds up a Stream recipe
– メソッドチェーン
• eager
– 例) count
– 戻り値: 値 または void
– 終端メソッド
7
What’s Actually Going On (2)
• Example 3-4 Just the filter, no collect step
• Example 3-5 Just the filter, no collect step
• Example 3-6 Printing out artist names
allArtists.stream() .filter( artist -> artist.isFrom("London") );
allArtists.stream() .filter(artist -> { System.out.println(artist.getName()); return artist.isFrom("London"); });
long count = allArtists.stream() .filter(artist -> { System.out.println(artist.getName()); return artist.isFrom("London"); }) .count();
printlnが実行されない
printlnが実行される
8
Common Stream Operations
• Stream のメソッドの使用例:
– collect
– map
– filter
– flatMap
– min(または max)
– reduce
• A Common Pattern Appears
• Putting Operations Together
9
collect(toList())
• Stream から 値のListを生成
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList()); assertEquals(Arrays.asList("a", "b", "c"), collected);
10
Stream を生成するファクトリメソッド
map (1)
• Stream内の値を、別の値に変換
• 変換後の値で、別のStreamを生成
• Example 3-8 文字列を大文字に変換: for ループ使用
List<String> collected = new ArrayList<>(); for (String string : Arrays.asList("a", "b", "hello")) { String uppercaseString = string.toUpperCase(); collected.add(uppercaseString); }
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);
11
map (2)
• Example 3-9 文字列を大文字に変換: map使用
• Figure 3-4. The Function interface
– 引数が1個(T)、戻り値が1個(R) のFunctionインタフェースをlamda式でmapの引数に指定
(この例では、引数:String, 戻り値:String で同じ型であったが、別の型になってもよい)
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(Collectors.toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);
12
filter • Example 3-10 数字から始まる文字を検索: forループ と if文 使用
• Example 3-11 数字から始まる文字を検索: 「Functional style」 filter使用
– if文と同じ働きをする1個のfunction を lamda式でfilterの引数に指定
– 戻り値: true OR false
– Figure 3-6. The Predicate interface
List<String> beginningWithNumbers = new ArrayList<>();
for(String value : Arrays.asList("a", "1abc", "abc1")) { if ( Character.isDigit(value.charAt(0)) ) { beginningWithNumbers.add(value); } } assertEquals(Arrays.asList("1abc"), beginningWithNumbers);
List<String> beginningWithNumbers
= Stream.of( "a", "1abc", "abc1" )
.filter( value -> Character.isDigit(value.charAt(0)) )
.collect( Collectors.toList() );
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);
13
flatMap • 値 を Stream に変換
• 変換後の全てのStreamを連結
• Example 3-12. Stream list
• mapとの共通点:
– Function を引数にとる
• mapとの違い:
– Function の戻り値(R) は Stream に制限される
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4) ) .flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
assertEquals(Arrays.asList(1, 2, 3, 4), together);
(Map) a value -> a new value (flatMap) a value -> a new Stream
List<Integer>
-> Stream<Integer> に変換
(Collection#stream メソッドを使って )
14
max and min
• Example 3-13. 長さが一番短い曲を検索: min 使用
• min や max を考えるのに、 「順序」 を考える必要がある
– 「順序」 をStreamに伝える → Comparator を使用 • この例では、順序に track.getLength を使用
• java8 で Comparator クラスに comparing という static メソッドが追加された
– 引数も戻り値もfunction
• 引数: Function
• 戻り値: Comparator
• min, max の戻り値: Optional (-> 第4章参照)
– get メソッドで値を取得
List<Track> tracks = Arrays.asList(new Track("Bakai", 524), new Track("Violets for Your Furs", 378), new Track("Time Was", 451));
Track shortestTrack = tracks.stream() .min( Comparator.comparing( track -> track.getLength() ) ) .get();
assertEquals(tracks.get(1), shortestTrack);
15
A Common Pattern Appears • Example 3-14. 長さが一番短い曲を検索: for ループ使用 (Example 3-13を書き換え)
– 変数(shortestTrack )を初期化
– for ループで if文
• If( 変数 と currnetの要素を比較。currentの要素の方が短ければ ) { 変数を書き換え }
– 最終的に 変数(shortestTrack )に一番短い曲が格納されている
• Example 3-15. The reduce pattern (擬似コード)
– If文の代わりに、combine 関数を呼び出し
• 一番短い曲を求める場合は combine は accumulator とcurrnetの要素を比較 して、短い方を返す
• この一般的なパターンが Streams API の operation で体系化されている
Track shortestTrack = tracks.get(0); for (Track track : tracks) { if (track.getLength() < shortestTrack.getLength()) { shortestTrack = track; } }
Object accumulator = initialValue; for(Object element : collection) { accumulator = combine(accumulator, element); }
16
reduce (1)
• collection の複数の値から、1つの結果 を生成
– count, min, max, sum, …
• Example 3-16. 要素の足し算. reduceを使用
• 変数 acc が accumulator の役割で 足し算の結果を保持
• 前回までの acc の値と、current の要素を足した結果が戻り値 -> 新しい acc
int count = Stream.of(1, 2, 3)
.reduce( 0, (acc, element) -> acc + element );
assertEquals(6, count);
T, T T BinaryOperator 初期値
17
reduce (2)
• Example 3-18. 要素の足し算. Imperative implementation
– 変数の全ての更新を手動で管理
int acc = 0;
for (Integer element : Arrays.asList(1, 2, 3)) {
acc = acc + element; }
assertEquals(6, acc);
18
Putting Operations Together
• 課題を、シンプルなStreamのoperationに分解
– (例)あるアルバムの中の band の国籍一覧を求める
• アルバムの中のミュージシャン一覧から
• 名前が "The" から始まるミュージシャンを band として扱い
• その band の国籍を求め、
• 国籍一覧を返す
• この domain クラス(Album) には 戻り値がStream のメソッドがあるが、それが無いクラスの場合
– List or Set でOK。 List or Set (Collection) に stream メソッドがあるため。
• domain クラスをカプセル化するのに…
– 戻り値がStream のメソッドを公開するほうが better
• ↑ List or Setが戻り値のメソッドを公開するよりも
• 内部の List or Set に影響を与えないため
Set<String> origins = album.getMusicians() .filter( artist -> artist.getName().startsWith("The") ) .map( artist -> artist.getNationality() ) .collect( Collectors.toSet() );
戻り値: Stream (lazy)
Artist -> String に変換して
新しい Stream を返す
19
Refactoring Legacy Code (Legacy code)
• forループをStreamに変更:
– step別にリファクタリングする例
• Legacy code : forループ ↓
• step 1: Stream#forEach を使用 ↓
• step 2: 内側の forEachブロックを分解 ↓
• step 3: ネストを解消 ↓
• step 4: 結果保持用の変数を削除
– (例) アルバム一覧から、1分より長い曲の曲名を求める
• Example 3-19. Legacy code
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
for(Album album : albums) { for (Track track : album.getTrackList()) { if (track.getLength() > 60) { String name = track.getName(); trackNames.add(name); } } }
return trackNames; } 20
Refactoring Legacy Code (step 1)
• Example 3-20. Refactor step 1: forループ から Stream#forEach 使用に変更
– 上記コードを洗練させるためのターゲット → 内側の forEachブロック
• 3つの処理 に分解 → (step 2) 3つの Stream の operation を使用
– 1分より長い曲を探す → filter
– その曲の名前を取得。曲から曲名に変換 → map
– その曲の名前をSetに格納 → 次のstep ではまだ forEach を使用
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach( album -> { album.getTracks() .forEach( track -> { if (track.getLength() > 60) { String name = track.getName(); trackNames.add(name); } }); }); return trackNames; }
21
Refactoring Legacy Code (step 2)
• Example 3-21. Refactor step 2: 内側の forEachブロックを分解
– ネストを解消したい
• album → track の stream に変換 → (step3) flatMap 使用
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> { album.getTracks() .filter( track -> track.getLength() > 60 ) .map( track -> track.getName() ) .forEach( name -> trackNames.add(name) ); });
return trackNames;
}
22
Refactoring Legacy Code (step 3)
• Example 3-22. Refactor step 3: ネストを解消
– 自分でSetをnewして、各elementをaddしている
• → (step 4) collect(toSet()) を使用
– 変数 trackNames を消すことができる
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.flatMap( album -> album.getTracks() ) .filter( track -> track.getLength() > 60 ) .map( track -> track.getName() ) .forEach( name -> trackNames.add(name) );
return trackNames;
}
23
Refactoring Legacy Code (step 4)
• Example 3-23. Refactor step 4: 結果保持用の変数を削除
• まとめ
– 段階的にリファクタリング
– 各step毎にユニットテストし続けて確認
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap( album -> album.getTracks() ) .filter( track -> track.getLength() > 60 ) .map( track -> track.getName() ) .collect( Collectors.toSet() ); }
24
Multiple Stream Calls • Example 3-24. Stream の誤用
• Example 3-25. Idiomatically chained stream calls
• Example 3-24がExample 3-25より悪い理由
– ビジネスロジックに対して boilerplate code の割合が悪くて読みづらい
– 各中間処理でcollectionオブジェクトを作成しているので非効率
– 各中間処理でしか必要としないゴミ変数がメソッドを汚くする
– 自動並列化が難しくなる
List<Artist> musicians = album.getMusicians() .collect( Collectors.toList() );
List<Artist> bands = musicians.stream() .filter( artist -> artist.getName().startsWith("The") ) .collect( Collectors.toList());
Set<String> origins = bands.stream() .map( artist -> artist.getNationality() ) .collect( Collectors.toSet() );
Set<String> origins = album.getMusicians() .filter( artist -> artist.getName().startsWith("The") ) .map( artist -> artist.getNationality() ) .collect( Collectors.toSet() );
25
Higher-Order Functions
• 高階関数 – 他の function を引数や戻り値に出来る関数
– シグニチャを見た時に、functional interface が引数や戻り値に使われていたら、そのメソッドは高階関数。
– 例えば mapメソッドは 引数に function を取るので高階関数。
– Streamのほとんど全ての関数は高階関数
– 前に出てきたソートの例では、comparing 関数は、他の関数を引数に取るだけでなく、戻り値としてComparatorをnewして返していた
– Comparator は object と思うかもしれないが、ただ1つの抽象関数を持っている。
• → functional interface
26
Good Use of Lambda Expressions (1)
• この章で紹介したコンセプト
– よりシンプルなコードを書く
– operation をデータに記述している
• what (何を変換) > how (どのように変換)
– バグが発生する可能性を低くするコード
– プログラマの意図が分かるコード
– 副作用の無い関数
» 何の値 (what values) を返しているのか見るだけで、
» その関数が何をして いるのか (what the functions are doing)
» が理解できる
27
Good Use of Lambda Expressions (2)
– 副作用の無い関数 • 外側から状態を変化させない
• 本書の最初のラムダの例では副作用のある例も載っている
– コンソールに print out
» 観察できる副作用
• では次の例では?
– 変数への代入、という副作用を作り出している
– プログラムの出力からは見えないかもしれないが、
» プログラムの状態を変更している
private ActionEvent lastEvent; private void registerHandler() { button.addActionListener( (ActionEvent event) -> { this.lastEvent = event; } ); }
28
Good Use of Lambda Expressions (3)
– この例ではローカル変数に代入しようとしている
• → 実際はコンパイルできない
• capture values > capturing variables
– ラムダ式で使用する場合、ローカル変数(上記例ではlocalEvent)に final キーワードが必要ない
• けれども、実質的には final (第2章)
• ラムダ式を Stream の高階関数に渡す時はいつも、副作用が無いことを目指すべき
– 唯一の例外: forEach メソッド
• Internal iteration は collection 側に iterate する処理を委譲して、collection を iterate する手段
• Stream は Internal iteration
• collection の多くの共通 operation は、ラムダ式を使って、Stream のメソッドを組み合わせる事によって実行される
ActionEvent localEvent = null; button.addActionListener(event -> { localEvent = event; });
Key Points
29