Java 8 Lambdas: Pragmatic Functional Programming
- 作者: Richard Warburton
- 出版社/メーカー: O'Reilly Media
- 発売日: 2014/03/18
- メディア: Kindle版
- この商品を含むブログを見る
前回メモっとけ Java 8 Lambdas 〜Chapter2〜 - A Memorandumからの続き
Stream
Stream は複雑なコレクション操作を関数型アプローチで構築するツールです。
外部あるいは内部の繰り返し
ロンドン出身のアーティスとを数える旧来のコード
int count = 0; for (Artist artist : allArtists) { if (artist.isFrom("London")) { count++; } }
繰り返し処理の抽象である Iterator を使ったコード
int count = 0; Iterator<Artist> iterator = allArtists.iterator(); while (iterator.hasNext()) { Artist artist = iterator.next(); if (artist.isFrom("London")) { count++; } }
上記は、Stream を使い以下のように書ける。
long count = allArtists.stream() .filter(artist -> artist.isFrom("London")) .count();
ループ処理が完全にライブラリ側に移動した。
Stream の挙動
以下の処理は、アーティスト名を表示しない
allArtists.stream() .filter(artist -> { System.out.println("artist.getName()"); artist.isFrom("London"); });
以下のようにするとアーティスト名は出力される
long count = allArtists.stream() .fillter(artist -> { System.out.println("artist.getName()"); return artist.isFrom("London"); }) .count();
先の例は Stream 操作が lazy となっているためである。戻り値がStreamではなく、他の戻り値か void の場合は eager となる。
Stream の処理がそれぞれのメソッドチェーンで処理されるのではなく、builder パターンのように、最後の結果を返すメソッドにおいて評価が行われる。
共通 Stream 操作
collect(toList())
Stream から listを生成する。 以下は "a", "b", "c" の List を返す。
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList());
ちなみにcollectの定義はこんな感じ。
<R, A> R collect(Collector<? super T, A, R> collector);
で、toList() の定義はこんな感じ
public static <T> Collector<T, ?, List<T>> toList() { return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add, (left, right) -> { left.addAll(right); return left; }, CH_ID); }
map
map は Stream の要素に関数を適用する。 以下はリストの文字を upperCase する従来の例
List<String> collected = new ArrayList<>(); for (String string : asList("a", "b", "hello")) { String uppercaseString = string.toUpperCase(); collected.add(uppercaseString); }
map を使うと以下のように書ける
List<String> collected = Stream.of("a", "b", "hello") .map(string -> string.toUpperCase()) .collect(toList());
filter
filter は Stream の要素に述語を適用して要素を絞り込む。 以下はリストの文字列から数字で始まるものを抽出する例
List<String> biginningWithNumbers = new ArrayList<>(); for (String value : asList("a", "1abc", "abc1")) { if (isDigit(value.charAt(0))) { biginningWithNumbers.add(value); } }
filter を使うと以下のように書ける
List<String> biginningWithNumbers = Stream.of("a", "1abc", "abc1") .filter(value -> isDigit(value.charAt(0))) .collect(toList());
flatMap
flatMap は Stream のリストを平坦化する。
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4)) .flatMap(numbers -> numbers.stream()) .collect(Collectors.toList()); assertThat(together, is(Arrays.asList(1, 2, 3, 4)));
max と min
最大とったり最小とったり
List<Track> tracks = Arrays.asList( new Track("Time Was", 451), new Track("Bakai", 524)); Track shortest = tracks.stream() .min(Comparator.comparing(track -> track.getLength())) .get();
戻り値は Optional
reduce
結果を次の入力に。1+2+3
int count = Stream.of(1, 2, 3) .reduce(0, (acc, element) -> acc + element); assertThat(count, is(6));
レガシーコードのリファクタリング
ネストの深いレガシーコードのリファクタリング例。 複数のアルバムから長さが60を超えるトラックを得る。
public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>(); for (Albums album : albums) { for (Track track : albums.getTracks()) { if (track.getLength() > 60) { String name = track.getName(); trackNames.add(name); } } } return trackNames; }
まずはforループをforEachに
public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>(); albums.stream() .forEach(album -> { albums.getTracks().forEach(track -> { if (track.getLength() > 60) { String name = track.getName(); trackNames.add(name); } }); }); return trackNames; }
if をフィルタに変えてmap
public Set<String> findLongTracks(List<Album> albums) { Set<String> trackNames = new HashSet<>(); albums.stream() .forEach(album -> { albums.getTracks() .filter(track -> track.getLength() > 60) .map(track -> track.getName()) .forEach(name -> trackNames.add(name)); }); return trackNames; }
flatMap 化して
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; }
最終的に
public Set<String> findLongTracks(List<Album> albums) { return albums.stream() .flatMap(album -> album.getTracks()) .filter(track -> track.getLength() > 60) .map(track -> track.getName()) .collect(toSet()); }