当サイトは、アフィリエイト広告を利用しています
javaのStreamAPIのcollectメソッドについて普段
toListやtoMapなどで使っているが、特に意味を考えることなく
利用していたので仕組みを調べてみた。
下記の本では streamAPI についても詳しく書いてありおススメです!
また当ブログで紹介しているjava8のstreamAPIを使った
コレクションや配列の操作方法を
下記記事でメソッド別にまとめています!
collectメソッドはStreamAPIで集計や編集したものに対して可変リダクション処理を行うStreamインターフェースのメソッド。
collectメソッドは引数にCollectorsクラスやCollectorインターフェースを取る。
Collectorsクラスには終端操作の代表的な操作がまとめられているので
だいたいはそれを利用するだけで事足りる。
また、Collectorsクラスのメソッドではやりたい処理が実現できない場合は
collectメソッドに直接、可変リダクション処理をかいたり、
Collectorインターフェースを実装した独自Collectorを作って可変リダクション処理をさせることもできる。
下記のような書き方ある。
Collectorsクラスのリダクション操作(toListなど)やCollectorインターフェースを実装して
独自をCollectorを作った場合はCollectorインターフェース型の引数を一つ設定する
// 構文collect(Collector collector)// Collectorsクラス利用// Listに変換~.collect(Collectors.toList())// Mapに変換~.collect(Collectors.toMap(Person::getName,Person::getWeight ,(e1,e2)->e1,LinkedHashMap::new));// 独自Collectorクラス(sampleCollector)利用~.collect(sampleCollector)// 独自Collectorインターフェースのofメソッドの利用~.collect(Collector.of(~))
collectメソッド内で可変リダクション処理を書く場合は引数は3つになり、
型は全て関数型インターフェースの型になる。
// 構文collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)// Mapにする可変リダクション処理~.collect(// 中間コンテナを生成// 最初の1回だけ実行()->new HashMap<>(),// 中間コンテナに値をセットしていく// streamの要素の数だけ実行される(Map m,Person p)->{m.put(p.getName(),p.getWeight());},// 中間コンテナ同士をマージする// 順次ストリームの場合未実行(m,m2)->{m.putAll(m2);});
Stream APIの特殊なメソッドとメソッド参照/コンストラクター参照
StreamAPIで集計や編集したものを最終的な結果(MapやList)などに変換すること
つまり、下記のCollectorsクラスのメソッドなどが可変リダクション処理にあたる。
CollectorsクラスとはStreamAPIの結果を処理するため、collectメソッドの引数として利用されるクラス。
よく使うもので下記のようなものがある。
可変リダクション操作のメソッドを定義したインターフェース。
Collector は「関数を返す4つのメソッド」と「特性を返すメソッド」がある。
まずは独自Collectorを作る時に実装する4つのメソッドをまとめる。
中間コンテナを生成する関数
型は関数型インターフェースのSupplier< A >
順次処理の時に1回だけ実行される。並列処理の時は複数回実行されることがある。
まず最初に可変リダクション処理をする上での入れ物を作るイメージ。
中間コンテナへ値を折り畳む関数
型は関数型インターフェースのBiConsumer
Stream の要素の数だけ実行される。
中間コンテナに値を追加していくイメージ
ふたつの中間コンテナをひとつにマージする関数。
型は関数型インターフェースのBinaryOperator
並列処理のときに各スレッドで生成された中間コンテナをを一つにする。
順次ストリームの場合は実行されない。
中間コンテナから最終的な結果へ変換する関数
型は関数型インターフェースのFunction
最後の1回だけ実行される。
よくわからない。後日追記する。
Collectorインターフェースを利用して独自のCollectorを作成することができる。
独自Collectorの作成方法は二つある
今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~
Streamのcollectメソッドを学ぶ
インタフェースCollector<T,A,R>
上記のことを踏まえて下記のパターンで可変リダクション処理を実装してみる。
PersonオブジェクトのListをLinkedHashMapに変換するサンプル
import java.util.*;import java.util.stream.Collectors;public class Main {public static void main(String[] args) throws Exception {class Person {private final String name;private final int weight;Person(String name, int weight) {this.name = name;this.weight = weight;}public String getName() {return this.name;}public int getWeight() {return this.weight;}}List<Person> personList = Arrays.asList(new Person("太郎", 72),new Person("二郎", 79),new Person("二郎", 99));// オブジェクトリストをMapに変換するLinkedHashMap<String,Integer>personMap =personList.stream().collect(Collectors.toMap(Person::getName,Person::getWeight ,(e1,e2)->e1,LinkedHashMap::new));// 変換したMapの中身をループで出力するpersonMap.entrySet().stream().forEach(map->{System.out.println(map.getKey()+"のエントリー");System.out.println("Mapのkey : " + map.getKey());System.out.println("Mapのvalue : " + map.getValue());});}}// 太郎のエントリー// Mapのkey : 太郎// Mapのvalue : 72// 二郎のエントリー// Mapのkey : 二郎// Mapのvalue : 79
toMapの使い方については下記記事で
詳しくまとめています
PersonオブジェクトのListをからPersonオブジェクトのnameだけのListを作る
import java.util.*;import java.util.stream.Collectors;public class Main {public static void main(String[] args) throws Exception {class Person {private final String name;private final int weight;Person(String name, int weight) {this.name = name;this.weight = weight;}public String getName() {return this.name;}public int getWeight() {return this.weight;}}List<Person> personList = Arrays.asList(new Person("太郎", 72),new Person("二郎", 79),new Person("二郎", 99));// オブジェクトリストを名前だけのList変換するList<String>nameList =personList.stream().map(obj->obj.getName()).collect(Collectors.toList());// 変換したListの中身をループで出力するnameList.stream().forEach(n->{System.out.println(n);});}}// 太郎// 二郎// 二郎
PersonオブジェクトのListをHashMapに変換するサンプル。
上記のtoMapの処理をCollectorsクラスのtoMapメソッドを使わず書いた感じ。
import java.util.*;import java.util.stream.Collectors;public class Main {public static void main(String[] args) throws Exception {class Person {private String name;private String weight;Person(String name, String weight) {this.name = name;this.weight = weight;}public String getName() {return this.name;}public String getWeight() {return this.weight;}public void setWeight(String weight) {this.weight = weight;}}List<Person> personList = Arrays.asList(new Person("太郎", null),new Person("二郎", "79"),new Person("三郎", "99"));// オブジェクトリストをMapに変換するMap<String,String>personMap =personList.stream().collect(// supplier// 中間コンテナを生成// 最初の1回だけ実行()->{System.out.println("supplier");return new HashMap<>();},// accumulator// 中間コンテナに値をセットしていく// stermの要素の数だけ実行される(Map m,Person p)->{System.out.println("accumulator");m.put(p.getName(),p.getWeight());},// 中間コンテナをマージする関数// 順次ストリームの場合は実行されない(m,m2)->{m.putAll(m2);System.out.println("combiner");});// 変換したMapの中身をループで出力するpersonMap.entrySet().stream().forEach(map->{System.out.println(map.getKey()+"のエントリー");System.out.println("Mapのkey : " + map.getKey());System.out.println("Mapのvalue : " + map.getValue());});}}// 実行結果// supplier// accumulator// accumulator// accumulator// 二郎のエントリー// Mapのkey : 二郎// Mapのvalue : 79// 太郎のエントリー// Mapのkey : 太郎// Mapのvalue : null// 三郎のエントリー// Mapのkey : 三郎// Mapのvalue : 99
出力を見ると下記のことがわかる
Collectorインターフェースのofメソッドを利用したパターンを実装してみる
引数を元にCollectorを生成するメソッド。
引数が3つのものと4つのものがある。
Collectorインターフェースの関数を返す4つのメソッド
に対応した関数をofメソッドの引数に渡す。
ただし引数が3つの場合はfinisherがないため、
supplier,accumulator,combinerで同一の型しか扱えない
of(Supplier<R> supplier,//中間コンテナを生成する関数BiConsumer<R,T> accumulator,//中間コンテナへ値を折り畳む関数BinaryOperator<R> combiner,//ふたつの中間コンテナをひとつにマージする関数。Collector.Characteristics... characteristics)
Collectorのジェネリクスは左から
// Collector<T,R,R>Collector<Person ,List<String>, List<String>> sampleCollector =Collector.of(// supplier()->new ArrayList<String>(),//ArrayList<Person>::new,//accumulator(list, presonObj)->{list.add(presonObj.getName());},//combiner(list1, list2) -> {list1.addAll(list2);return list1;});
collectorはList< Person >からPerson.nameだけのList< String >を作る
combinerは順次ストリームの時は動かない
引数が4つの場合はfinisherがあるため、 中間コンテナの値を最終的な結果へ変換することができる
of(Supplier<A> supplier,//中間コンテナを生成する関数BiConsumer<A,T> accumulator,//中間コンテナへ値を折り畳む関数BinaryOperator<A> combiner,//ふたつの中間コンテナをひとつにマージする関数。Function<A,R> finisher,//中間コンテナから最終的な結果へ変換する関数Collector.Characteristics... characteristics)
Collectorのジェネリクスは左から
// Collector<T,A,R>Collector<Person ,List<Person>, Map<String,String>> sample =Collector.of(// supplier()->new ArrayList<Person>(),// accumulator(list, presonObj)->{list.add(presonObj);},// combiner(list1, list2) -> {list1.addAll(list2);return list1;},// finisher(list)->list.stream().collect(Collectors.toMap(e->e.getName(),e->e.getWeight(),(e1,e2)->e1)));
collectorはList< Person >からMap< name, Weight >のMapを作る
combinerは順次ストリームの時は動かない
ofメソッドを使って実装してみる
import java.util.*;import java.util.stream.*;public class Main {public static void main(String[] args) throws Exception {class Person {private String name;private String weight;Person(String name, String weight) {this.name = name;this.weight = weight;}public String getName() {return this.name;}public String getWeight() {return this.weight;}public void setWeight(String weight) {this.weight = weight;}}List<Person> personList = Arrays.asList(new Person("太郎", "22"),new Person("二郎", "79"),new Person("三郎", "99"));// Collector<T,R,R>// 引数が3つのofメソッドでcollectorを作成Collector<Person ,List<String>, List<String>> sampleCollector1 =Collector.of(// supplier()->new ArrayList<String>(),//ArrayList<Person>::new,//accumulator(list, presonObj)->{list.add(presonObj.getName());},//combiner(list1, list2) -> {list1.addAll(list2);return list1;});// Collector<T,A,R>// 引数が4つのofメソッドでcollectorを作成Collector<Person ,List<Person>, Map<String,String>> sampleCollector2 =Collector.of(// supplier()->new ArrayList<Person>(),// accumulator(list, presonObj)->{list.add(presonObj);},// combiner(list1, list2) -> {list1.addAll(list2);return list1;},// finisher(list)->list.stream().collect(Collectors.toMap(e->e.getName(),e->e.getWeight(),(e1,e2)->e1)));// 実行System.out.println("sampleCollector1を使う");List<String>sampleList = personList.stream().collect(sampleCollector1);sampleList.stream().forEach(name->{System.out.println(name);});System.out.println("sampleCollector2を使う");Map<String,String>sampleMap = personList.stream().collect(sampleCollector2);sampleMap.entrySet().stream().forEach(entry->{System.out.println(entry.getKey());System.out.println(entry.getValue());});System.out.println("すべてチェーンする");// ListからMap(中間コンテナ)を通してSetにするpersonList.stream().collect(Collector.of(// supplierHashMap<String,String>::new,// accumulator(map,pobj)->map.put(pobj.getName(),pobj.getWeight()),// combiner(map1,map2)->{map1.putAll(map2);return map1;},// finisher(map)->{Set<String>res = new HashSet<>();map.entrySet().stream().forEach(key->res.add(key.getKey()));return res;})).stream().forEach(val->{System.out.println(val);});}}
実行して、結果を出力する。
// sampleCollector1を使う// 太郎// 二郎// 三郎// sampleCollector2を使う// 二郎// 79// 三郎// 99// 太郎// 22// すべてチェーンする// 二郎// 太郎// 三郎
独自作成したcollectorが正常に動作してることがわかる。
エラー情報
【Java】【Stream API】オレオレCollectorによるヘッダ・明細Bean生成で学ぶ独自Collectorの実装方法
Streamのcollectメソッドを学ぶ
Collector インターフェースを実装して独自collectorを作る
インターフェースの関数を返す4つのメソッドを実装する
※
import java.util.ArrayList;import java.util.HashSet;import java.util.LinkedHashSet;import java.util.List;import java.util.Map;import java.util.HashMap;import java.util.Set;import java.util.function.BiConsumer;import java.util.function.BinaryOperator;import java.util.function.Function;import java.util.function.Supplier;import java.util.stream.Collector;// Map<String,String>をオブジェクトのリストにするcollector// Collector<T,A,R>public class SampleCollector implements Collector<Map.Entry<String, String>, Map<String,String>, List<Person>> {// 中間コンテナ生成メソッド// Supplier< A> supplier@Overridepublic Supplier<Map<String,String>> supplier() {// 引数なし、戻り値ありの関数を返すreturn () -> new HashMap<>();}// 中間コンテナへ値を折り畳む関数// BiConsumer< A,T> accumulator@Overridepublic BiConsumer<Map<String,String>, Map.Entry<String, String>> accumulator() {// 引数が二つ、戻り値なしの関数を返すreturn (acumMap, entry) -> {// Integer aa = Integer.parseInt(pObj.getWeight());acumMap.put(entry.getKey(),entry.getValue());};};// ふたつの中間コンテナをひとつにマージする関数// 順儒ストリームの場合は動かない// BinaryOperator< A > combiner@Overridepublic BinaryOperator<Map<String,String>> combiner() {//引数が二つで戻り値ありの関数を返すreturn (returnMap, colletedMap) -> {System.out.println("combiner!");returnMap.putAll(colletedMap);return returnMap;};}// 中間コンテナから最終的な結果へ変換する関数// Function< A,R > finisher@Overridepublic Function<Map<String,String>, List<Person>> finisher() {// 一つの引数ありで戻り値ありの関数を返す// 戻り値の型を中間コンテナから変更することができるreturn paramMap -> {List<Person>pList = new ArrayList<>();paramMap.entrySet().stream().forEach(entry->{pList.add(new Person(entry.getKey(),Integer.parseInt(entry.getValue())));});return pList;};}@Overridepublic Set<Collector.Characteristics> characteristics() {final Set<Collector.Characteristics> returnSet = new HashSet<>();// 3つの指定ができますが、サンプルではOFFです。// returnSet.add(Characteristics.IDENTITY_FINISH);// returnSet.add(Characteristics.CONCURRENT);// returnSet.add(Characteristics.UNORDERED);return returnSet;}}
SampleCollectorを使って可変リダクション処理をしてみる
import java.util.*;import java.util.stream.*;public class Main {public static void main(String[] args) throws Exception {// mapを作るMap<String,String>personMap = new HashMap<String,String>(){{ put("森博嗣", "11"); }{ put("周期律", "22"); }{ put("辻村深月", "33"); }};// 独自コレクターをインスタンス化SampleCollector collector = new SampleCollector();// collectを実行List<Person>pList = personMap.entrySet().stream().collect(collector);// 実行System.out.println("Collector実装クラスを使う");pList.stream().forEach(obj->{System.out.println(obj.getName());System.out.println(obj.getWeight());});}}// 実行結果// Collector実装クラスを使う// 森博嗣// 11// 辻村深月// 33// 周期律// 22
StreamAPIについて調べてみた collect編 その1
最後に少し並列streamについても書いておく。
parallel()をつけることでstreamで並列処理をすることができる
ただ順番等は保証されないため注意が必要。
import java.util.*;import java.util.stream.*;public class Main {public static void main(String[] args) throws Exception {// mapを作るList<Integer> List = Arrays.asList(1,2,3,4,5,6,7,8,9,10);// 実行List.stream().parallel().forEach(no->{System.out.println(no);});}}
collectメソッドで可変リダクション処理をする場合に
「BinaryOperator< A > combiner」というふたつの中間コンテナをひとつにマージする関数を設定するが
この処理はparallel()をつけた場合のみ実行される
import java.util.*;import java.util.stream.Collectors;public class Main {public static void main(String[] args) throws Exception {class Person {private String name;private String weight;Person(String name, String weight) {this.name = name;this.weight = weight;}public String getName() {return this.name;}public String getWeight() {return this.weight;}public void setWeight(String weight) {this.weight = weight;}}List<Person> personList = Arrays.asList(new Person("太郎", null),new Person("二郎", "79"),new Person("三郎", "99"));// オブジェクトリストをMapに変換する// parallelをつけて並列ストリームにするMap<String,String>personMap =personList.stream().parallel().collect(// supplier// 中間コンテナを生成// 最初の1回だけ実行()->{System.out.println("supplier");return new HashMap<>();},// accumulator// 中間コンテナに値をセットしていく// stermの要素の数だけ実行される(Map m,Person p)->{System.out.println("accumulator");m.put(p.getName(),p.getWeight());},// 中間コンテナをマージする関数// 並列ストリームの場合のみ実行される(m,m2)->{System.out.println("combiner");m.putAll(m2);});// 変換したMapの中身をループで出力するpersonMap.entrySet().stream().forEach(map->{System.out.println(map.getKey()+"のエントリー");System.out.println("Mapのkey : " + map.getKey());System.out.println("Mapのvalue : " + map.getValue());});}}// 実行結果// supplier// accumulator// supplier// accumulator// supplier// accumulator// combiner// combiner// 二郎のエントリー// Mapのkey : 二郎// Mapのvalue : 79// 太郎のエントリー// Mapのkey : 太郎// Mapのvalue : null// 三郎のエントリー// Mapのkey : 三郎// Mapのvalue : 99
大分ややこしいので改めて整理してみる。
下記のクラスやインターフェースが登場する
インターフェースStream
インタフェースCollector
クラスCollectors
全部で5つある