当サイトは、アフィリエイト広告を利用しています

【java8】streamのcollectメソッドの使い方

作成日:2022月04月06日
更新日:2023年12月14日

javaのStreamAPIのcollectメソッドについて普段
toListやtoMapなどで使っているが、特に意味を考えることなく
利用していたので仕組みを調べてみた。

下記の本では streamAPI についても詳しく書いてありおススメです!

また当ブログで紹介しているjava8のstreamAPIを使った
コレクションや配列の操作方法を
下記記事でメソッド別にまとめています!

collectメソッドとは?

collectメソッドはStreamAPIで集計や編集したものに対して可変リダクション処理を行うStreamインターフェースのメソッド。
collectメソッドは引数にCollectorsクラスやCollectorインターフェースを取る。
Collectorsクラスには終端操作の代表的な操作がまとめられているので
だいたいはそれを利用するだけで事足りる。
また、Collectorsクラスのメソッドではやりたい処理が実現できない場合は collectメソッドに直接、可変リダクション処理をかいたり、
Collectorインターフェースを実装した独自Collectorを作って可変リダクション処理をさせることもできる。

基本構文

下記のような書き方ある。

Collectorsクラスのメソッドまたは独自Collectorを使う場合

Collectorsクラスのリダクション操作(toListなど)やCollectorインターフェースを実装して
独自をCollectorを作った場合はCollectorインターフェース型の引数を一つ設定する

java
// 構文
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メソッドで可変リダクション処理を書く場合

collectメソッド内で可変リダクション処理を書く場合は引数は3つになり、
型は全て関数型インターフェースの型になる。

java
// 構文
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);
}
);
  • T - 入力要素の型
  • R - 中間蓄積の型(中間コンテナ)
  • R - 最終結果の型
  • 中間蓄積の型とR最終結果の型が同一になる。要は中間コンテナの型からと同一の型しかできない。
  • 3つ目の引数の関数combinerについては順次ストリームの場合は使用されない。

インタフェースStream< T >

Stream APIの特殊なメソッドとメソッド参照/コンストラクター参照

(補足)可変リダクション処理って??

StreamAPIで集計や編集したものを最終的な結果(MapやList)などに変換すること
つまり、下記のCollectorsクラスのメソッドなどが可変リダクション処理にあたる。

Java8 Streamのリダクション操作について

Collectorsクラスとは?

CollectorsクラスとはStreamAPIの結果を処理するため、collectメソッドの引数として利用されるクラス。
よく使うもので下記のようなものがある。

  • toListメソッド
  • toMapメソッド
  • toSetメソッド
  • joiningメソッド

クラスCollectors

Collectorインターフェースとは?

可変リダクション操作のメソッドを定義したインターフェース。
Collector は「関数を返す4つのメソッド」と「特性を返すメソッド」がある。

関数を返す4つのメソッド

まずは独自Collectorを作る時に実装する4つのメソッドをまとめる。

supplier

中間コンテナを生成する関数
型は関数型インターフェースのSupplier< A >
順次処理の時に1回だけ実行される。並列処理の時は複数回実行されることがある。
まず最初に可変リダクション処理をする上での入れ物を作るイメージ。

accumulator

中間コンテナへ値を折り畳む関数
型は関数型インターフェースのBiConsumer
Stream の要素の数だけ実行される。
中間コンテナに値を追加していくイメージ

combiner

ふたつの中間コンテナをひとつにマージする関数。 型は関数型インターフェースのBinaryOperator
並列処理のときに各スレッドで生成された中間コンテナをを一つにする。
順次ストリームの場合は実行されない。

finisher

中間コンテナから最終的な結果へ変換する関数 型は関数型インターフェースのFunction
最後の1回だけ実行される。

特性を返すメソッド

よくわからない。後日追記する。

  • CONCURRENT
  • IDENTITY_FINISH
  • UNORDERED

独自Collectorの作成方法

Collectorインターフェースを利用して独自のCollectorを作成することができる。
独自Collectorの作成方法は二つある

  • Collector の ofメソッド を利用する
  • Collector インターフェースを実装したクラスを作る

参考

今さら聞けないJavaによる関数型プログラミング入門 ~ラムダ式、ストリーム、関数型インターフェース~

Streamのcollectメソッドを学ぶ
インタフェースCollector<T,A,R>

可変リダクション処理実装してみる

上記のことを踏まえて下記のパターンで可変リダクション処理を実装してみる。

  1. collectメソッドでCollectorsクラスのメソッドを使う
  2. collectメソッドで可変リダクション処理を行う
  3. collectメソッドでCollector の ofメソッド を利用して作成したインスタンスを使う(独自Collector)
  4. collectメソッドでCollector インターフェースを実装したクラスのインスタンスを使う(独自Collector)

1.Collectorsクラスのメソッドを使う

PersonオブジェクトのListをLinkedHashMapに変換するサンプル

toMap
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の使い方については下記記事で
詳しくまとめています

toList

PersonオブジェクトのListをからPersonオブジェクトのnameだけのListを作る

java
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);
});
}
}
// 太郎
// 二郎
// 二郎

2.collectメソッドで可変リダクション処理を行う

PersonオブジェクトのListをHashMapに変換するサンプル。
上記のtoMapの処理をCollectorsクラスのtoMapメソッドを使わず書いた感じ。

java
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

出力を見ると下記のことがわかる

  • supplierは初回のみ実行
  • accumulatorはstreamの要素数回実行
  • 順次ストリームのためcombinerは未実行

3.collectメソッドでCollector の ofメソッド を利用して作成したインスタンスを使う(独自Collector)

Collectorインターフェースのofメソッドを利用したパターンを実装してみる

Collectorインターフェースのofメソッドとは?

引数を元にCollectorを生成するメソッド。
引数が3つのものと4つのものがある。

ofメソッド(引数3つ)

Collectorインターフェースの関数を返す4つのメソッド
に対応した関数をofメソッドの引数に渡す。
ただし引数が3つの場合はfinisherがないため、
supplier,accumulator,combinerで同一の型しか扱えない

基本構文
of(Supplier<R> supplier,//中間コンテナを生成する関数
BiConsumer<R,T> accumulator,//中間コンテナへ値を折り畳む関数
BinaryOperator<R> combiner,//ふたつの中間コンテナをひとつにマージする関数。
Collector.Characteristics... characteristics)
  • Supplier :引数なし、returnあり
  • BiConsumer :引数二つ、returnなし(引数二つの型は同じ)
  • BinaryOperator :引数二つ、returnあり(型は引数と戻り値同じ)

ofメソッド(引数3つ)のサンプル

Collectorのジェネリクスは左から

  • T:streamの要素の型
  • R:中間コンテナの型
  • R:最終結果の型 になる
java
// 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 >を作る

  • Personクラスの要素を持つstreamに対して使える(List< Person >など)
  • 中間コンテナはList< String >型で作る。
  • 最終結果はList< String >型で返す

combinerは順次ストリームの時は動かない

ofメソッド(引数4つ)

引数が4つの場合はfinisherがあるため、 中間コンテナの値を最終的な結果へ変換することができる

java
of(Supplier<A> supplier,//中間コンテナを生成する関数
BiConsumer<A,T> accumulator,//中間コンテナへ値を折り畳む関数
BinaryOperator<A> combiner,//ふたつの中間コンテナをひとつにマージする関数。
Function<A,R> finisher,//中間コンテナから最終的な結果へ変換する関数
Collector.Characteristics... characteristics)
  • Function :引数1つ、returnあり(型は引数と戻り値でなくてもいい)

ofメソッド(引数4つ)のサンプル

Collectorのジェネリクスは左から

  • T:streamの要素の型
  • A:中間コンテナの型
  • R:最終結果の型
java
// 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を作る

  • Personクラスの要素を持つstreamに対して使える(List< Person >など)
  • 中間コンテナはList< Person >型で作る。
  • 最終結果はMap< String,String >型で返す

combinerは順次ストリームの時は動かない

ofメソッドを実装

ofメソッドを使って実装してみる

java
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(
// supplier
HashMap<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);
});
}
}

実行して、結果を出力する。

java
// sampleCollector1を使う
// 太郎
// 二郎
// 三郎
// sampleCollector2を使う
// 二郎
// 79
// 三郎
// 99
// 太郎
// 22
// すべてチェーンする
// 二郎
// 太郎
// 三郎

独自作成したcollectorが正常に動作してることがわかる。

参考

エラー情報
【Java】【Stream API】オレオレCollectorによるヘッダ・明細Bean生成で学ぶ独自Collectorの実装方法
Streamのcollectメソッドを学ぶ

4.collectメソッドでCollector インターフェースを実装したクラスのインスタンスを使う

Collector インターフェースを実装して独自collectorを作る
インターフェースの関数を返す4つのメソッドを実装する

  • Supplier< A> supplier,     //中間コンテナを生成する関数
  • BiConsumer< A,T> accumulator, //中間コンテナへ値を折り畳む関数
  • BinaryOperator< A > combiner, //ふたつの中間コンテナをひとつにマージする関数
  • Function< A,R > finisher,   //中間コンテナから最終的な結果へ変換する関数

  • T:streamの要素の型
  • A:中間コンテナの型
  • R:最終結果の型

Collector インターフェースを実装したクラスを作成

  • Mapを受け取り、List< Person >の形にするcollectorを自作する
SampleCollector.java
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
@Override
public Supplier<Map<String,String>> supplier() {
// 引数なし、戻り値ありの関数を返す
return () -> new HashMap<>();
}
// 中間コンテナへ値を折り畳む関数
// BiConsumer< A,T> accumulator
@Override
public 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
@Override
public BinaryOperator<Map<String,String>> combiner() {
//引数が二つで戻り値ありの関数を返す
return (returnMap, colletedMap) -> {
System.out.println("combiner!");
returnMap.putAll(colletedMap);
return returnMap;
};
}
// 中間コンテナから最終的な結果へ変換する関数
// Function< A,R > finisher
@Override
public 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;
};
}
@Override
public 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を使って可変リダクション処理をしてみる

main.java
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について

最後に少し並列streamについても書いておく。

streamを並列処理する

parallel()をつけることでstreamで並列処理をすることができる
ただ順番等は保証されないため注意が必要。

java
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);
});
}
}

collctメソッドとの関連

collectメソッドで可変リダクション処理をする場合に
「BinaryOperator< A > combiner」というふたつの中間コンテナをひとつにマージする関数を設定するが
この処理はparallel()をつけた場合のみ実行される

java
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

インターフェースStream
インタフェースCollector
クラスCollectors

Streamの可変リダクション処理のパターン

全部で5つある

1.【collectメソッドの引数にCollectorsクラスのメソッドを渡す】

  • collectメソッドにCollectorsクラスのメソッドを使うパターン
  • toMapやtoListなど基本使うやつはだいたいCollectorsクラスにある

2.【collectメソッドで直接処理する】

  • collectメソッド内に直接処理を書くパターン。
  • 引数が三つなので中間コンテナの型からの変更はできない。
  • 3との違いは3つめの引数(combiner)の型が違い、こちらはBiConsumer型のため戻り値を返す必要がない。

3.【collectメソッドの引数にCollectorインターフェースのofメソッドで関数(引数3つ)を渡す】

  • collectメソッドの引数をofメソッドにするパターン。
  • 引数が三つなので中間コンテナの型からの変更はできない。
  • 2との違いは3つめの引数(combiner)の型が違い、こちらはBinaryOperator型のため戻り値を返す必要がある

4.【collectメソッドの引数にCollectorインターフェースのofメソッドで関数(引数4つ)を渡す】

  • collectメソッドの引数をofメソッドにするパターン。
  • 4つ目の引数にfinisherがあるため、中間コンテナの型から最終結果の型に変えることができる

5.【collectメソッドの引数にCollectorインターフェースを実装したクラス(引数4つ)のインスタンスを渡す】

  • Collectorインターフェースを実装したクラスを作るパターン
  • 動作としては「collectメソッドの引数にCollectorインターフェースのofメソッドで関数(引数4つ)を渡す」と同じ

新着記事

目次
タグ別一覧
top