メモっとけ Java 8 Lambdas 〜Chapter3〜


Java 8 Lambdas: Pragmatic Functional Programming

Java 8 Lambdas: Pragmatic Functional Programming

        

前回メモっとけ 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());
    }