Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다. 기존에 표준 라이브러리를 통해 제공되는 Collector로도 가능하지만 직접 커스텀한 Collector를 만들어 리듀싱 연산을 제공할수도 있다.
소수 분류 문제를 커스텀 Collector 구현으로 성능 개선을 해보자.
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
<T, A, R>
Supplier<A> supplier();
: 비어있는 새로운 결과 컨테이너를 제공하는 Supplier를 반환해야 한다. 즉, 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수이다.
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
//return ArrayList::new; //메서드 참조로 제공할 수도 있다.
}
BiConsumer<A, T> accumulator();
: 리듀싱 연산을 수행하는 함수를 반환한다. 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다. 함수의 반환값은 void로 요소를 탐색하며 적용하는 함수에 의해 누적자 내부상태가 바뀌기에 누적자를 단정할 수 없다.
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
//return List::add;
}
BinaryOperator<A> combiner();
: 리듀싱 연산에서 사용할 함수를 반환한다. 스트림의 서로 다른 서브 파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다. 우리가 자주 사용하는 toList의 combiner는 대략적으로 다음과 같이 구현되어 있다.
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
Function<A, R> finisher();
: 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하며 누적 과정을 끝낼 때 호출할 함수를 반환하는 메서드. 누적자 객체가 이미 최종 결과인 경우도 있는데 이런 경우 항등 함수를 반환하면 된다.
public Function<A, R> finisher(){
return Function.identity();
}
Set<Characteristics> characteristics();
: 컬렉션의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다. 이 열거 타입은 스트림을 병렬로 리듀스할지, 병렬로 리듀스한다면 어떤 최적화를 선택해야 할 지에 대한 힌트를 제공한다.
UNORDERED
CONCURRENT
IDENTITY_FINISH
전달해준 숫자를 최대값으로 그 사이의 값 중 소수 목록과 그외 목록을 분류해서 반환해주는 기능을 만든다고 하자. 이 때 별도의 Custom Collector를 구현하지 않고, 이 기능을 구현한다면 대략 이런식으로 작성할 수 있을 것 같다.