49
CHAPTER 5 Advanced Collections and Collectors Kei Takinami

Java8 Lambda chapter5

Embed Size (px)

DESCRIPTION

Java8 Lambdas勉強会 第5章

Citation preview

Page 1: Java8 Lambda chapter5

CHAPTER 5 Advanced Collections and Collectors

Kei Takinami

Page 2: Java8 Lambda chapter5

Method References  メソッド参照

Java8 から「 :: 」演算子を使ってメソッドを関数オブジェクトとして参照することができるようになった。

// インスタンスメソッド String#length()

Function<String, Integer> func1 = String::length;

assert   func1.apply("Java 8") == 6;

インターフェイス: Function<T,R> 実装するメソッド: R apply(T t)        概要:実装するメソッドは、引数として T を受け取り、結果として R を返すものになる。

メモ

Page 3: Java8 Lambda chapter5

Example1

例 )  アーティストの名前の一覧を取得したい場合。

artist -> artist.getName()

あくまでメソッド参照なので、最後の () はいらない。

メモ

メソッド参照の場合

ラムダ形式の場合

Artist::getName

Page 4: Java8 Lambda chapter5

Example2

コンストラクタでも使える

(name, nationality) -> new Artist(name, nationality)

メソッド参照の場合

ラムダ形式の場合

Artist::new

Page 5: Java8 Lambda chapter5

メソッド参照の形式

// static メソッドの場合public class Sample {

public static void main(String[] args) {

Function<String, String> func2 = Sample::add;

System.out.println(func2.apply(“test02")); // => [ ☆ test02 ☆]

}

public static String add(String value) {

return “[ " + value + " ]";☆ ☆ }

}

クラス名 :: メソッド名  (static メソッド )

インスタンス名 :: メソッド名  ( インスタンスメソッド )

Page 6: Java8 Lambda chapter5

// インスタンスメソッドの場合public class Sample {

public static void main(String[] args) {

Sample sample = new Sample();

Function<String, String> func1 = sample::add;

System.out.println(func1.apply(“test01")); // => [ ☆ test01 ☆]

}

public static String add(String value) {

return “[ " + value + " ]";☆ ☆ }

}

Page 7: Java8 Lambda chapter5

Element Ordering  要素の順番

順序が定義された Collection から Stream を生成するときにはその Stream も順序を持つ。

List<Integer> numbers = asList(1,2,3,4);

List<Integer> sameOrder = numbers.stream()

                         .collect(toList());

assertEquals(numbers, sameOrder);   // => これは必ず OK

例 )  リストと Stream から生成された同じリストを比較

Page 8: Java8 Lambda chapter5

逆に順序が定義されてない Collection( 例えば HashSet) から Stream を生成するときにはその Stream も順序を持たない。

Set<Integer> numbers = new HashSet<>(asList(4,3,2,1));

List<Integer> sameOrder = numbers.stream()

                         .collect(toList());

// NG になる場合があるassertEquals(asList(4,3,2,1) sameOrder);  

例 )   Set と Stream から生成された同じリストを比較

Page 9: Java8 Lambda chapter5

Stream の役割

Stream の役割はある Collection を他の Collection へ変換するだけじゃない。データに対していろんな処理ができるようにすることでもある。

Set<Integer> numbers = new HashSet<>(asList(4,3,2,1));

List<Integer> sameOrder = numbers.stream()

.sorted()

.collect(toList());// 前回と違って今回は必ず OK

assertEquals(asList(1,2,3,4), sameOrder);

例 )  順番

Page 10: Java8 Lambda chapter5

この順序は途中で処理を入れても維持される。例えば map 処理を途中に入れてみる。

List<Integer> numbers = asList(1,2,3,4);

List<Integer> stillOrdered = numbers.stream()

       .map(x -> x + 1)

.collect(toList());

assertEquals(asList(2,3,4,5), stillOrdered); // => 必ず OK

例 )   map 処理で対象のリストをインクリメント

Page 11: Java8 Lambda chapter5

ただ、順序があるから必ずいいというわけではない。順序があることによってコストがかかることもある。そういう場合は、 stream の unordered メソッドを使用

List<Integer> numbers = asList(1,2,3,4);

List<Integer> stillOrdered = numbers.stream()

       . unordered ()   // 追加

       .map(x -> x + 1)

.collect(toList());

assertEquals(asList(2,3,4,5), stillOrdered); // => 必ず OK

例 )  前の例に unordered() を追加

でも実際、 filter,map,reduce は順序を維持した stream でもかなり高速に動作する。

メモ

Page 12: Java8 Lambda chapter5

parallelstream のときだけは forEach は順番を保証しない。

List<String> list = Arrays.asList("list1", "list2", "list3", "list4", "list5");

// 順番通りに表示 => list1, list2, list3, list4, list5

list.stream().forEach(e -> System.out.println(e));

// ランダムに表示 => list3, list5, list4, list2, list1

list.parallelStream().forEach(e -> System.out.println(e));

並行処理の場合の順序

Page 13: Java8 Lambda chapter5

その場合は   forEachOrdered(Consumer<? super T> action)

List<String> list = Arrays.asList("list1", "list2", "list3", "list4", "list5");

// ランダムに表示 => list3, list5, list4, list2, list1

list.parallelStream().forEach(e -> System.out.println(e));

// ランダムに表示 => list3, list5, list4, list2, list1

list.parallelStream(). forEachOrdered(e -> System.out.println(e));

※ 注意順序は保証してくれるがパフォーマンスに影響があるのでparallelstream のときだけ使用するように。

Page 14: Java8 Lambda chapter5

今まで collect(toList()) を使って List を作成しているけどmap も Set も使いたい。

Stream<String> s = Stream.of("a", "b", "c");

Set<String> set = s.collect(Collectors.toSet());

System.out.println(set);

Enter the Collector  Collector について

toSet() も toMap() も toCollection もある

例 )   toSet()

Page 15: Java8 Lambda chapter5

ただ、 toList のときも同様になんの List かわからない。

stream().collect(toCollection(TreeSet::new));

自動的に最適な実クラスを Stream ライブラリが設定していくれている。

例 )   TreeSet を指定

でも、ライブラリに任せないで自分でこのリストを使いたいときがある。

Page 16: Java8 Lambda chapter5

collector を使って、ひとつの値を取得することもできる。

public static Optional<Artist> biggestGroup(Stream<Artist> artists) {

   Function<Artist, Long> getCount = artist -> artist.getMembers()

             .count();

   // 一番メンバーの多いバンドを返却   return   artists.collect(Collectors.maxBy(Comparator.comparing(getCount)));

}

To Values 値の変換

例 )   maxBy

・ maxBy

・ minBy

Page 17: Java8 Lambda chapter5

もう少し簡単な例で maxBy

public static void sample01() {

   Stream<Integer> stream = Stream.of(1,2,3,4,5);

   Optional<Integer> maxNumber =

            stream.collect(Collectors.maxBy(Comparator.naturalOrder()));

   System.out.println(maxNumber.get());

}

結果: 5

public static void sample02() {

   Stream<String> stream = Stream.of(“a”,“b”,“c”);   // 文字列も OK

   Optional<String> maxNumber =

             stream .collect(Collectors.maxBy(Comparator.naturalOrder()));

   System.out.println(maxNumber.get());

}

結果: c

Page 18: Java8 Lambda chapter5

他にも合計とか平均も出せる便利なものもある【 1 】

// 合計を取得する場合  =>   summingInt

public static void sample01() {

   Stream<Integer> stream = Stream.of(1,2,3,4,5);

   ToIntFunction<Integer> func = x -> x;

   // 合計を取得   int sum = stream.collect(Collectors.summingInt(func));

   // 15 を出力   System.out.println(sum);

}

Page 19: Java8 Lambda chapter5

他にも合計とか平均も出せる便利なものもある【 2 】

// オブジェクトを取得して、いろいろ計算public static void sample02() {

   Stream<Integer> numberStream = Stream.of(1,2,3,4,5);

   ToIntFunction<Integer> func = x -> x;

   // 集計オブジェクトを取得 => summarizingInt で取得   IntSummaryStatistics sum = numberStream.collect(Collectors.summarizingInt(func));

   // 合計を出力 (15)

   System.out.println(sum.getSum());

   // 要素数を出力 (5)

   System.out.println(sum.getCount());

   // 最大値を出力 (5)

   System.out.println(sum.getMax());

   // 最小値を出力 (1)

   System.out.println(sum.getMin());

   // 平均を出力 (3)

   System.out.println(sum.getAverage());

}

Page 20: Java8 Lambda chapter5

Stream を使って条件毎に2つのコレクションに変換したい場合がある。例)アーティストのリストをソロとバンドで分けたい場合

// partitioningBy メソッドを使って分割public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {

   return artists.collect(Collectors.partitioningBy(artist -> artist.isSolo()));

}

結果: true=[SoloA,SoloB] false=[BandA]

// メソッド参照を使った場合public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) {

   return artists.collect(Collectors.partitioningBy(Artist::isSolo));

}

Partitoning the Data データの分割

partitioningBy を使えばおk。引数には Predicate を指定して、結果が true の場合と false の場合で場合分けしてくれる。

インターフェイス: Predicate<T>実装するメソッド: boolean test(T t)     概要:実装するメソッドは、引数として T を受け取り、 boolean 値を結果として返すものになる。

メモ

Page 21: Java8 Lambda chapter5

true,false で分けるんじゃなくて、他にどんな値でもできる。例えば、アルバムをアーティスト毎に分類したい場合。

public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {

  return albums.collect(Collectors.groupingBy(album -> album.getMainMusician()));

}

Grouping the Data データの分類

groupingBy を使えばおk。引数には Classifier を指定してあげる。

Page 22: Java8 Lambda chapter5

Stream のデータを一般的に使う理由としては最終的に文字列を生成するため。 ( が多い )

Strings 文字列

Page 23: Java8 Lambda chapter5

public static void main(String args[]) {

   List<Artist> artists = new ArrayList<>();

   artists.add(new Artist("George"));

   artists.add(new Artist("Harrison"));

   artists.add(new Artist("John"));

   StringBuilder builder = new StringBuilder("[");

   for (Artist artist : artists) {

     if (builder.length() > 1) {

       builder.append(",");

     }

     String name = artist.getName();

     builder.append(name);

   }

   builder.append("]");

  String result = builder.toString();

System.out.println(result);   // => [George,Harrison,John]

}

例えばアーティスト名の一覧を出力したい場合

public static void main(String args[]) {

   List<Artist> artists = new ArrayList<>();

   artists.add(new Artist("George"));

   artists.add(new Artist("Harrison"));

   artists.add(new Artist("John"));

   String result = artists.stream()

               .map(Artist::getName)

               .collect(Collectors.joining(“,”,“[”,“]”));

   System.out.println(result); // => [George,Harrison,John]

}

Java8 以前の場合 Java8 の場合

Collectors.joining メソッドを使えばお k

Collectors.joining( 区切り文字 , プレフィックス , サフィックス ) といった感じ

Page 24: Java8 Lambda chapter5

前回はアルバムをアーティスト毎に分類したが 今回は各アーティストのアルバムの数を集計したい。

Composing Collectors  Collectors の合成

一番簡単な方法は一度グルーピングしてから、アルバム数をカウント。

Page 25: Java8 Lambda chapter5

// アーティスト毎にグルーピング Map<Artist, List<Album>> albumsByArtist = albums.collect(groupingBy(album ->

                                         album.getMainMusician()));

// グルーピングした Map から個数を取得 Map<Artist, Integer> numberOfAlbums = new HashMap<>();

for(Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {

   numberOfAlbums.put(entry.getKey(), entry.getValue().size()); // => [ アーティスト:枚数 ]

}

例 ) 一度グルーピングしてから、アルバム数をカウント。

// アーティスト毎にグルーピングpublic Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {

return albums.collect(groupingBy(album ->

album.getMainMusician(), Collectors .counting()));

}

counting() を使って同じ処理を簡単に

Page 26: Java8 Lambda chapter5

Stream<String> s = Stream.of("a", "bar", "c", "foo");

Map<Integer, String> m = s.collect(Collectors.groupingBy(t -> t.length()

                                              , Collectors.joining()));

System.out.println(m);   // => {1=ac, 3=barfoo}

他の例

こんな感じで groupingBy の第2引数の Collector を「 downstream collectors 」と呼ぶ

主に第一引数の結果になにかしらの処理を行いときに使う。averagingInt,summarizingLong, もそういった意味では「 downstream collectors 」の一種

Page 27: Java8 Lambda chapter5

今度は個数じゃなく、アルバム名が欲しい場合

public Map<Artist, List<String>> nameOfAlbumsDumb(Stream<Album> albums) {

Map<Artist, List<Album>> albumsByArtist = albums.collect(

                               groupingBy(album ->album.getMainMusician()));

Map<Artist, List<String>> nameOfAlbums = new HashMap<>();

for(Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {

  nameOfAlbums.put(entry.getKey(), entry.getValue()

             .stream()

             .map(Album::getName)

             .collect(toList()));

}

return nameOfAlbums;

}

前回同様グルーピングしてから、マッピング

Page 28: Java8 Lambda chapter5

「 mapping 」を使って簡単に

public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {

return albums.collect(groupingBy(

        Album::getMainMusician, mapping(Album::getName, toList())));

}

Page 29: Java8 Lambda chapter5

java7 で書かれた以下のソースを独自の Stringjoiningcollector を使ってjava8 のソースへリファクタしていこう。※JDK 自体は今回作る collector を既に提供している。今回はあくまで、勉強のために。

StringBuilder builder = new StringBuilder("[");

for (Artist artist : artists) {

  if (builder.length() > 1) {

   builder.append(", ");

}

String name = artist.getName();

  builder.append(name);

}

builder.append("]");

String result = builder.toString();  

結果 = [aa,bb,cc] といった感じ

Refactoring and Custom Collectors リファクタリングと独自 Collectors

Page 30: Java8 Lambda chapter5

第1ステップ

StringBuilder builder = new StringBuilder("[");

artists.stream()

     .map(Artist::getName)

     .forEach(name -> {

       if (builder.length() > 1)

         builder.append(", ");

       builder.append(name);

     });

builder.append("]");

String result = builder.toString();

stream と map を使った修正してみる

for 分の中が大きいし、まだまだ読みにくい。

Page 31: Java8 Lambda chapter5

第 2 ステップ

StringBuilder reduced =   artists.stream()

                    .map(Artist::getName)

                    .reduce(new StringBuilder(), (builder, name) -> {

                       if (builder.length() > 0) {

                         builder.append(", ");

}

                       builder.append(name);

                       return builder;

                    }, (left, right) -> left.append(right));

reduced.insert(0, "[");

reduced.append("]");

String result = reduced.toString();

reduce を使ってみる

よけい読みにくく。。

Page 32: Java8 Lambda chapter5

第 3 ステップ

StringCombiner combined = artists.stream()

.map(Artist::getName)

.reduce(new StringCombiner(", ", "[", "]"),

StringCombiner::add,

StringCombiner::merge);

String result = combined.toString();

独自の StringCombiner クラスを生成前回と違って、結合処理は全部このクラスに隠して実装

Page 33: Java8 Lambda chapter5

第 3.1 ステップ

public StringCombiner add(String element) {

if (areAtStart()) {

builder.append(prefix);

} else {

builder.append(delim);

}

builder.append(element);

return this;

}

StringCombiner の add メソッドの中身

Page 34: Java8 Lambda chapter5

第 3.2 ステップ

public StringCombiner merge(StringCombiner other) {

builder.append(other.builder);

return this;

}

StringCombiner の merge メソッドの中身

Page 35: Java8 Lambda chapter5

第 3.3 ステップ

String combined = artists.stream()

.map(Artist::getName)

.reduce(new StringCombiner(", ", "[", "]"),

StringCombiner::add,

StringCombiner::merge)

.toString();

String result = combined;

最後のメソッドチェインになるように toStringを追加

Page 36: Java8 Lambda chapter5

第 4 ステップ

String result = artists.stream()

               .map(Artist::getName)

               .collect(new StringCollector(", ", "[", "]"));

ただ、これを汎用的に使うのは難しい。なので汎用的に使えるように reduce 処理を Collector にしてしまう。今回それを StringCollector として新しく作成。

これで完全に独自 Collector を作ることができて、シンプルに。他の Collector とまったく同じ形式で使える。

Page 37: Java8 Lambda chapter5

早速 StringCollector を実装してみる

Page 38: Java8 Lambda chapter5

その前に。。。

Page 39: Java8 Lambda chapter5

Collector インターフェイスを implement

独自 Collector の作り方

public interface Collector<T, A, R> {

~}

< 処理対象の型、実処理の型、返却値の型 >

例えば「 Collector<String, ?, Integer> 」だと、 Stream<String> から Integer を生成する。

Page 40: Java8 Lambda chapter5

Collector インターフェイスに定義されている4 つのメソッドを実装してあげる。

メソッド名 戻り値の型 ラムダ式表現 内容supplier Supplier<A> () -> A → 前処理 要素の集積に使うオブジェクトを生成する

accumulator BiConsumer<A,T> (A, T) -> () → (A)集積 集積オブジェクト と、Stream 1の 要素を引数に、集積オブジェクトに要素を集積する

combiner BinaryOperator<A> (A,A) -> A → 2 1結合 並列処理の結果 つから つの結果にまとめる

finisher Function<A,R> A -> R → 後処理 集積オブジェクトを最後に変換する

Page 41: Java8 Lambda chapter5

では、実際に StringCollector を実装

Page 42: Java8 Lambda chapter5

1. supplier

public Supplier<StringCombiner> supplier() {

   return () -> new StringCombiner(delim, prefix, suffix);

}

前に作った StringCombiner を使って

イメージ的には横の図パラレルで動作することもあるので Supplier は 2 つ。

supplier でコンテナオブジェクトを作成している。( ここでいうコンテナオブジェクトはStringCombiner)

Page 43: Java8 Lambda chapter5

2.accumulator

public BiConsumer<StringCombiner, String> accumulator() {

   return StringCombiner::add;

}

Supplier と同様に前に作った StringCombiner を使って

Stream の値をコンテナオブジェクトに追加している。

Page 44: Java8 Lambda chapter5

3. combine

public BinaryOperator<StringCombiner> combiner() {

   return StringCombiner::merge;

}

同様に前に作った StringCombiner を使って

combine で並行で処理していたものを統一。この際には新しい Container に登録している。

Page 45: Java8 Lambda chapter5

4. finsher

public Function<StringCombiner, String> finisher() {

   return StringCombiner::toString;

}

toString の値を最終的な戻り値として生成

finsher で最終的な成果物を生成

Page 46: Java8 Lambda chapter5

完成

public class StringCollector implements Collector<String, StringCombiner, String> {

   public Supplier<StringCombiner> supplier() {

     return () -> new StringCombiner(delim, prefix, suffix);

   }

   public BiConsumer<StringCombiner, String> accumulator() {

     return StringCombiner::add;

   }

   public BinaryOperator<StringCombiner> combiner() {

     return StringCombiner::merge;

   }

   public Function<StringCombiner, String> finisher() {

     return StringCombiner::toString;

   }

}

Page 47: Java8 Lambda chapter5

1.Map を使ってキャッシュしていた実装も  ラムダが導入されたお陰でシンプルに。

Collection Niceties

public Artist getArtist (String name) {

   Artist artist = artistCache.get(name);

   if (artist == null) {

    artist = readArtistFromDB(name);

    artistCache.put(name, artist);

   }

return artist;

}

Map に存在しない場合は DB に問い合わせるキャッシュの例

public Artist getArtist(String name) {

   return artistCache.computeIfAbsent(name, this::readArtistFromDB);

}

computeIfAbsent メソッドで代替可能

Page 48: Java8 Lambda chapter5

2.Map を使っての foreach 分もシンプルに。

Map<Artist, Integer> countOfAlbums = new HashMap<>();

for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {

Artist artist = entry.getKey();

List<Album> albums = entry.getValue();

countOfAlbums.put(artist, albums.size());

}

Map<Artist, Integer> countOfAlbums = new HashMap<>();

albumsByArtist.forEach((artist, albums) -> {

   countOfAlbums.put(artist, albums.size());

});

Page 49: Java8 Lambda chapter5

まとめ

・メソッド参照は簡単な記述でメソッドの参照ができる。

・ Collector は reduce メソッドの可変な analogue だし、  Stream の最終的な値を操作できる。

・ Java8 から様々な Collection を集計でき、独自    Collector も作成できるようになった。