0
Help us understand the problem. What are the problem?

posted at

【Java】Collectorインタフェースの使用方法 

Collectorインタフェース

ストリーム外部のオブジェクトの状態を変更したい時に使用します。また、Collectorはストリームのリダクション操作のためのインターフェースです。リダクション操作とは一連の入力要素を受け取り、結合操作を繰り返し適用することで、単一のサマリー結果を出力します。例えば、一連の数値の合計や、最大値の合計値などを求めることが挙げられます。

なぜ必要なのか?

副作用がある処理の問題を解決できるのがCollectorインターフェースです。副作用のある処理とは、同じメソッドでもタイミングによって結果が変わってくる処理のことです。例えば、外部のオブジェクトにラムダ式の内部でアクセスする場合、そのオブジェクトは実質的にfinalとして扱われます。そのため、変数が保持している参照を変更していないかはコンパラーはチェックしていますが、参照先のオブジェクトが持つ値を変更しているかどうかはチェックしません。そのため、参照先のオブジェクトが持つ値を変更してもコンパイルエラーにはなりません。このような、副作用のある処理を解決するのがCollectorインターフェースなのです。

クラス定義方法

Collector インターフェースは、3つの型パラメータを受け取ります。
1つ目が、ストリーム内の要素の型、2つ目が処理途中の値を保持するためのオブジェクト、3つ目は最終的な結果の型です。

public class SampleCollector implements Collector <String,StringBuilder,String>{

5つの抽象メソッド

Collectorインターフェースは5つの抽象メソッドがあります。抽象メソッドが5つもあるのでまずは処理の全体像を把握しましょう。把握できましたら、具体的に一つずつ抽象メソッドを見ていきます。
スクリーンショット 2022-04-01 7.14.04.png

①supplierメソッド(供給)

処理途中の値を保持するためのオブジェクトを生成するためのメソッドです。
下記のコードはメソッド参照を使用して、StringBuilderのインスタンスを生成して戻しています。

@Override 
public Supplier<StringBuilder> supplier(){
    return StringBuillder::new;
}

②accumulatorメソッド(蓄積)

具体的に実行したい処理を記述したBiConsumer型のラムダ式を戻すメソッドです。
下記のコードは処理途中のカンマ区切りの文字列を蓄えるためのStringBuilder型と、ストリーム内の要素であるString型のジェネリックスの型パラメータとして受け取っています。builder,strという変数で引数を受け取っています。

@Override
public BiConsumer <StringBuilder,String>accumulator() {
  return(builder, str) ->{
	if(builder.length() != 0) {
		builder.append(",");
	}
	    builder.append(str);
  };
}

③combinerメソッド(結合)

並列処理をしているとき、ここに作られた処理途中の値を保持するためのオブジェクトを結合するためのメソッドです。

@Override
public BinaryOperator<StringBuilder> combiner(){
   return(a,b)->
      if(a.length() !=0){
         a.append(",");
      }
       a.append(b);
       return a;
}

④finisherメソッド(完走)

処理結果を戻すラムダ式を提供するメソッドです。
下記のコードは、カンマ区切りの文字列を蓄えたStringBuilderを引数に受け取り、String型の戻り値を戻すFunction型のラムダ式を戻します。

@Override 
public Function<StringBuilder, String> finisher(){
       return builder -> builder.toString();
}

⑤characteristicsメソッド(特徴)

Collectorの特徴を表すEnumのセットを戻すメソッドです。Collectorの特徴には3つ用意されています。

CONCURRENT
このCollectorが並行処理をすることを表しています。

IDNTITY_FINISH
このCollectorのfinisherメソッドが省略可能であることを表します。

UNORDERED
コレクションの操作において順序の維持を保障しないことを表します。

指定する場合がない場合は
EnumSetクラスのnoneOfメソッドにCollector.Characteristicsクラスのクラスリテラルを渡せば、Characteristics型を扱う空のSetオブジェクトが生成されます。

###サンプルコード

SampleCollector.java
import java.util.EnumSet;
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;

public class SampleCollector implements Collector <String,StringBuilder,String>{

	@Override
    //処理途中の値を保持するためのオブジェクトを生成する為に,supplierメソッドを使用
	public Supplier<StringBuilder> supplier() {
		return StringBuilder::new;
	}

	@Override
    //具体的に実行したい処理をする為にaccumulatorメソッドを使用
	public BiConsumer <StringBuilder,String>accumulator() {
		return(builder, str) ->{
			if(builder.length() != 0) {
				builder.append(",");
			}
			builder.append(str);
		};
	}

	@Override
    //処理途中の値を保持したオブジェクトを結合する為にcombinerメソッドを使用
	public BinaryOperator <StringBuilder>combiner() {
		return(a,b)->{
			if(a.length()!= 0) {
				a.append(",");
			}
			a.append(b);
			return a;
		};
	}

	@Override
    //処理結果を戻すラムダ式を取得する為にfinisherメソッドを使用
	public Function<StringBuilder,String> finisher() {
		return builder -> builder.toString();
	}

	@Override
    /*Correctorの特徴を指定 
     *このサンプルコードでは特徴を指定しないのでnoneOfメソッドを使用
    */
	public Set<Characteristics> characteristics() {
		return EnumSet.noneOf(Characteristics.class);
	}

}

ストリームでCollectorを扱うためにcollectメソッドを使用します。

Sample.java
import java.util.Arrays;
import java.util.List;

public class Sample {
	public static void main(String[] args) {
	List<String> list = Arrays.asList("A","B","C","D");

	String result = list.stream().collect(new SampleCollector());
	System.out.println(result);
	}
}
A,B,C,D

Collectorsクラス

CollectorsクラスはCollectorインターフェースの実現クラスで、要素をコレクションに蓄積したり、さまざまな条件に従って要素を要約したりできちゃう便利なクラスなんです。また、Collectorインターフェースの具象クラスが用意されています。使う場面ごとに説明して行きます。

どのように使うの?

新しいリストの詰め替え処理をしたい時

toListメソッドを使います。新しい詰め替え処理が簡単にできてしまいます!

ItemType.java
public enum ItemType {
	BOOK,MANGA
}
Item.java
public class Item {
	private int id;
	private ItemType type;
	private String name;
	private int price;

	public Item(int id, ItemType type, String name, int price) {
		super();
		this.id = id;
		this.type = type;
		this.name = name;
		this.price = price;
	}

		public int getId() {
			return id;
		}
		public ItemType getType() {
			return type;
		}
		public String getName() {
			return name;
		}
		public int getPrice() {
			return price;
		}
		@Override
		public String toString() {
			return "Item[id=" + id + ",type=" + type + ",name="+ name + ",price=" +price+ "]";
		}
}
Sample.java
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Sample {
	public static void main(String[] args) {
			List<Item> list = Arrays.asList(
					new Item(1, ItemType.MANGA, "鬼滅の刃", 480),
					new Item(2, ItemType.MANGA, "呪術廻戦", 480),
					new Item(3, ItemType.BOOK,"Java",1984)
					);
			//フィルタリングして、得た結果のみ構成されたリストを作成
			List<Item> manga = list.stream().filter(item ->item.getType() == ItemType.MANGA).collect(Collectors.toList());
			manga.forEach(System.out::println);
	}
}
Item[id=1,type=MANGA,name=鬼滅の刃,price=480]
Item[id=2,type=MANGA,name=呪術廻戦,price=480]

Mapを使いたい時

toMapメソッドを利用します。toMapは2つの引数を受け取ります。引数は以下の通りです。
・第一引数 キーを取り出すためのFunction型ラムダ式
・第二引数 値を取り出すためのFunction型ラムダ式

サンプルコードを通して具体的に説明していきます。サンプルコードでは先ほど作成したlistからtoMapメソッドで名前を取り出していきます。

Sample.java

package innerclass;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
	public static void main(String[] args) {
			List<Item> list = Arrays.asList(
					new Item(1, ItemType.MANGA, "鬼滅の刃", 480),
					new Item(2, ItemType.MANGA, "呪術廻戦", 480),
					new Item(3, ItemType.BOOK,"Java",1984)
					);
			/*toMapメソッドを使用して、リストから名前を取り出す
			 * 第一引数 名前を取り出すためのFunction型ラムダ式
			 * 第二引数 Item型のitemをそのまま戻すFunction型ラムダ式
			 */
			Map<String, Item> map = list.stream().collect(Collectors.toMap(Item::getName,item -> item));
			map.keySet().stream().forEach(System.out::println);
	}
}

その結果、リストから名前を取得することができました。

Java
鬼滅の刃
呪術廻戦

グループごとにリストを分けたい場合

groupingByメソッドを使用します。groupingByメソッドは、引数としてストリームの要素の型、戻り値型はラムダ式が戻す型になります。例えば、以下のサンプルコードでは、getTypeメソッドが戻すItemType型が戻されます。その結果ItemTypeごとにリストが作られています。

Sample.java
package innerclass;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
	public static void main(String[] args) {
			List<Item> list = Arrays.asList(
					new Item(1, ItemType.MANGA, "鬼滅の刃", 480),
					new Item(2, ItemType.MANGA, "呪術廻戦", 480),
					new Item(3, ItemType.BOOK,"Java",1984)
					);
			//groupingByメソッドを使用して、MANGAとBOOKに分ける
			Map<ItemType, List<Item>>group = list.stream().collect(Collectors.groupingBy(Item::getType));
			System.out.println(group);
	}
}
{BOOK=[Item[id=3,type=BOOK,name=Java,price=1984]], MANGA=[Item[id=1,type=MANGA,name=鬼滅の刃,price=480], Item[id=2,type=MANGA,name=呪術廻戦,price=480]]}

グループ内の要素が持つ数値の合計を求める場合

summingIntメソッドを使用します。引数には求めたい数値の持つ要素を指定します。例えば、サンプルコードでは料金の合計を引数に指定して、合計値を戻しています。

Sample.java
package innerclass;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
	public static void main(String[] args) {
			List<Item> list = Arrays.asList(
					new Item(1, ItemType.MANGA, "鬼滅の刃", 480),
					new Item(2, ItemType.MANGA, "呪術廻戦", 480),
					new Item(3, ItemType.BOOK,"Java",1984)
					);
			//groupingByメソッドを使用して、MANGAとBOOKに分け、タイプごとの合計値を求める
			Map<ItemType, Integer>group = list.stream().collect(Collectors.groupingBy(Item::getType,
					Collectors.summingInt(Item::getPrice)));
			System.out.println(group);
	}
}
{MANGA=960, BOOK=1984}

条件に沿ってに分けたい場合

partitioningByメソッドを利用します。Predicate型ラムダ式を指定します。
サンプルコードを通して、具体的な使用方法を見ていきましょう。

Sample.java
package innerclass;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
	public static void main(String[] args) {
			List<Item> list = Arrays.asList(
					new Item(1, ItemType.MANGA, "鬼滅の刃", 480),
					new Item(2, ItemType.MANGA, "呪術廻戦", 480),
					new Item(3, ItemType.BOOK,"Java",1984)
					);
			//partitioningByメソッドを使用して、1000円以上か以下に分ける
			Map<Boolean, List<Item>>group = list.stream().collect(
					Collectors.partitioningBy(item -> item.getPrice() > 1000));
			System.out.println(group);
	}
}

この結果1000円以上か以下で分けることができました。

{false=[Item[id=1,type=MANGA,name=鬼滅の刃,price=480], Item[id=2,type=MANGA,name=呪術廻戦,price=480]], true=[Item[id=3,type=BOOK,name=Java,price=1984]]}

まとめ

スクリーンショット 2022-04-01 7.48.53.png

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?