•
Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
•
지금까지 toList나 groupingBy 등 Collector 인터페이스도 있었고 직접 Collector 인터페이스를 구현하는 리듀싱 연산을 만들 수도 있다.
•
Collector 인터페이스를 직접 구현해서 더 효율적으로 문제를 해결하는 컬렉터를 만드는 방법을 알아보자.
•
Collector 인터페이스를 살펴보기 전에 toList를 자세히 확인해보자.
•
toList는 앞으로 일상에서 자주 사용하는 컬렉터 중 하나다. 동시에 가장 구현하기 쉬운 컬렉터이기도 하다.
•
toList가 어떻게 구현되었는지 살펴보면서 Collectors는 어떻게 정의되었고 내부적으로 collect 메서드는 toList가 반환하는 함수를 어떻게 활용하는지 알아보자.
•
다음 코드는 Collector 인터페이스의 시그니처와 다섯 개의 메서드 정의다.
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
Java
복사
◦
T는 수집될 스트림 항목의 제네릭 형식이다.
◦
A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
◦
R은 수집 연산 결과 객체의 형식이다.
•
예를 들어 Stream<T>의 모든 요소를 List<T>로 만들어주는 ToListCollector<T>라는 클래스를 구현할 수 있다.
public class ToListCollector<T> Implement Collector<T, List<T>, List<T>>>
Java
복사
1. Collector 인터페이스의 메서드 살펴보기
•
다섯 개의 메서드를 하나씩 살펴보자.
•
나머지는 collect 메서드에서 실행하는 함수를 반환하는 반면, characteristics는 collect 메서드가 어떤 최적화로 리듀싱을 진행할지 결정하도록 돕는 힌트 특성 집합을 제공한다.
supplier 메서드: 새로운 결과 컨테이너 만들기
•
supplier 메서드는 빈 결과로 이루어진 Supplier를 반환한다. supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.
•
ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
•
ToListCollector에서의 supplier는 다음처럼 빈 리스트를 반환한다.
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
Java
복사
accumulator 메서드 : 결과 컨테이너에 요소 추가하기
•
accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다.
•
스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n - 1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.
•
함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 변경되지 따로 반환값은 존재하지 않으므로 누적자가 어떤 타입의 값인지 단정지을 수 없다.
•
ToListCollector에서 accumulator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산을 수행한다.
public BiCounsumer<List<T>, T> accumulator() {
return List::add;
}
Java
복사
finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기
•
finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 종료할 때 호출할 함수를 반환한다.
•
ToListCollector에서 볼 수 있는 것처럼 누적자 객체 자체가 최종 결과인 상황도 있으며, 이 경우에는 변환 과정이 따로 필요하지 않으므로 항등 함수를 반환한다.
public Function<List<T>, List<T>> finisher() {
return Fucntion.identity();
}
Java
복사
combiner 메서드 : 두 결과 컨테이너 병합
•
combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
•
toList의 combiner는 비교적 쉽게 구현할 수 있다. 즉 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
Java
복사
•
이 메서드를 이용하면 스트림의 리듀싱을 병렬로 수행할 수 있다. 스트림의 리듀싱을 병렬로 수행할 때 자바 7의 포크/조인 프레임워크와 7장에서 배울 Spliterator를 사용한다.
characteristics 메서드
•
마지막으로 characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
•
Characteristics는 스트림을 병렬로 리듀스할 것인지, 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
•
Characteristics는 다음 세 항목을 포함하는 열거형이다.
◦
UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
⇒ 리스트의 순서는 상관이 없으므로 UNORDERED다.
◦
CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
▪
컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.
⇒ ToListCollector의 순서는 별 의미가 없기도하고 CONCURRENT다.
◦
IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순히 identity를 적용할뿐이므로 이를 생략할 수 있다. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다.
⇒ ToListCollector는 스트림의 요소를 누적하는 데 사용한 리스트가 최종 결과이므로 최종 변환이 필요없으니 IDENTITY_FINISH다.
2. 응용하기
•
지금까지 살펴본 다섯 가지 메서드를 이용해서 커스텀 ToListCollector를 구현할 수 있다.
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supllier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
};
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT));
}
}
}
Java
복사
•
컬렉터를 구현하지 않고도 커스팀 수집 수행하기
◦
IDENTITY_FINISH 수집 연산에서는 Collector 인터페이스를 완전히 새로 구현하지 않고도 같은 결과를 얻을 수 있다.
◦
Stream은 세 함수를 인수로 받는 collect 메서드를 오버로드하며 각각의 메서드는 Collecotr 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.
List<Dish> dishes = menuStream.collect(ArrayList::new, List::add, List::addAll);
Java
복사