java8 lambdas chap03

29
社内勉強会資料 Java 8 Lambdas 輪読会 3Streams 大槌 剛彦 (@ohtsuchi) 1

Upload: ohtsuchi

Post on 16-Apr-2017

420 views

Category:

Software


3 download

TRANSCRIPT

社内勉強会資料

Java 8 Lambdas 輪読会

第3章

Streams

大槌 剛彦 (@ohtsuchi)

1

第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