/////
Search
Duplicate
5️⃣

Collector 인터페이스

Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
지금까지 toListgroupingByCollector 인터페이스도 있었고 직접 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 메서드에서 실행하는 함수를 반환하는 반면, characteristicscollect 메서드가 어떤 최적화로 리듀싱을 진행할지 결정하도록 돕는 힌트 특성 집합을 제공한다.
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는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
toListcombiner는 비교적 쉽게 구현할 수 있다. 즉 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
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
복사