java8 lambda chapter5
Post on 17-Nov-2014
212 Views
Preview:
DESCRIPTION
TRANSCRIPT
CHAPTER 5 Advanced Collections and Collectors
Kei Takinami
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 を返すものになる。
メモ
Example1
例 ) アーティストの名前の一覧を取得したい場合。
artist -> artist.getName()
あくまでメソッド参照なので、最後の () はいらない。
メモ
メソッド参照の場合
ラムダ形式の場合
Artist::getName
Example2
コンストラクタでも使える
(name, nationality) -> new Artist(name, nationality)
メソッド参照の場合
ラムダ形式の場合
Artist::new
メソッド参照の形式
// 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 メソッド )
インスタンス名 :: メソッド名 ( インスタンスメソッド )
// インスタンスメソッドの場合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 + " ]";☆ ☆ }
}
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 から生成された同じリストを比較
逆に順序が定義されてない 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 から生成された同じリストを比較
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);
例 ) 順番
この順序は途中で処理を入れても維持される。例えば 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 処理で対象のリストをインクリメント
ただ、順序があるから必ずいいというわけではない。順序があることによってコストがかかることもある。そういう場合は、 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 でもかなり高速に動作する。
メモ
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));
並行処理の場合の順序
その場合は 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 のときだけ使用するように。
今まで 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()
ただ、 toList のときも同様になんの List かわからない。
stream().collect(toCollection(TreeSet::new));
自動的に最適な実クラスを Stream ライブラリが設定していくれている。
例 ) TreeSet を指定
でも、ライブラリに任せないで自分でこのリストを使いたいときがある。
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
もう少し簡単な例で 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
他にも合計とか平均も出せる便利なものもある【 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);
}
他にも合計とか平均も出せる便利なものもある【 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());
}
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 値を結果として返すものになる。
メモ
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 を指定してあげる。
Stream のデータを一般的に使う理由としては最終的に文字列を生成するため。 ( が多い )
Strings 文字列
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( 区切り文字 , プレフィックス , サフィックス ) といった感じ
前回はアルバムをアーティスト毎に分類したが 今回は各アーティストのアルバムの数を集計したい。
Composing Collectors Collectors の合成
一番簡単な方法は一度グルーピングしてから、アルバム数をカウント。
// アーティスト毎にグルーピング 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() を使って同じ処理を簡単に
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 」の一種
今度は個数じゃなく、アルバム名が欲しい場合
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;
}
前回同様グルーピングしてから、マッピング
「 mapping 」を使って簡単に
public Map<Artist, List<String>> nameOfAlbums(Stream<Album> albums) {
return albums.collect(groupingBy(
Album::getMainMusician, mapping(Album::getName, toList())));
}
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
第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 分の中が大きいし、まだまだ読みにくい。
第 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 を使ってみる
よけい読みにくく。。
第 3 ステップ
StringCombiner combined = artists.stream()
.map(Artist::getName)
.reduce(new StringCombiner(", ", "[", "]"),
StringCombiner::add,
StringCombiner::merge);
String result = combined.toString();
独自の StringCombiner クラスを生成前回と違って、結合処理は全部このクラスに隠して実装
第 3.1 ステップ
public StringCombiner add(String element) {
if (areAtStart()) {
builder.append(prefix);
} else {
builder.append(delim);
}
builder.append(element);
return this;
}
StringCombiner の add メソッドの中身
第 3.2 ステップ
public StringCombiner merge(StringCombiner other) {
builder.append(other.builder);
return this;
}
StringCombiner の merge メソッドの中身
第 3.3 ステップ
String combined = artists.stream()
.map(Artist::getName)
.reduce(new StringCombiner(", ", "[", "]"),
StringCombiner::add,
StringCombiner::merge)
.toString();
String result = combined;
最後のメソッドチェインになるように toStringを追加
第 4 ステップ
String result = artists.stream()
.map(Artist::getName)
.collect(new StringCollector(", ", "[", "]"));
ただ、これを汎用的に使うのは難しい。なので汎用的に使えるように reduce 処理を Collector にしてしまう。今回それを StringCollector として新しく作成。
これで完全に独自 Collector を作ることができて、シンプルに。他の Collector とまったく同じ形式で使える。
早速 StringCollector を実装してみる
その前に。。。
Collector インターフェイスを implement
独自 Collector の作り方
public interface Collector<T, A, R> {
~}
< 処理対象の型、実処理の型、返却値の型 >
例えば「 Collector<String, ?, Integer> 」だと、 Stream<String> から Integer を生成する。
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 → 後処理 集積オブジェクトを最後に変換する
では、実際に StringCollector を実装
1. supplier
public Supplier<StringCombiner> supplier() {
return () -> new StringCombiner(delim, prefix, suffix);
}
前に作った StringCombiner を使って
イメージ的には横の図パラレルで動作することもあるので Supplier は 2 つ。
supplier でコンテナオブジェクトを作成している。( ここでいうコンテナオブジェクトはStringCombiner)
2.accumulator
public BiConsumer<StringCombiner, String> accumulator() {
return StringCombiner::add;
}
Supplier と同様に前に作った StringCombiner を使って
Stream の値をコンテナオブジェクトに追加している。
3. combine
public BinaryOperator<StringCombiner> combiner() {
return StringCombiner::merge;
}
同様に前に作った StringCombiner を使って
combine で並行で処理していたものを統一。この際には新しい Container に登録している。
4. finsher
public Function<StringCombiner, String> finisher() {
return StringCombiner::toString;
}
toString の値を最終的な戻り値として生成
finsher で最終的な成果物を生成
完成
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;
}
}
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 メソッドで代替可能
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());
});
まとめ
・メソッド参照は簡単な記述でメソッドの参照ができる。
・ Collector は reduce メソッドの可変な analogue だし、 Stream の最終的な値を操作できる。
・ Java8 から様々な Collection を集計でき、独自 Collector も作成できるようになった。
top related