Collector란?
스트림의 collect 메서드에 Collector 인터페이스 구현을 넘겨 스트림의 요소를 어떤 식으로 도출할지 지정한다.
예를 들어 toList()는 리스트를 반환하도록 하고, groupingBy()는 주어진 키에 대응하는 리스트를 갖는 맵 형태로 반환하도록 한다.
고급 리듀싱 기능 수행
컬렉터의 최대 강점은 collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이다.
스트림에서 collect를 호출하면 리듀싱 연산 을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 자동으로 작업을 처리해준다.
보통 함수를 요소로 변환할 때 컬렉터를 적용하며, 최종 결과를 저장하는 자료구조에 값을 누적한다.
미리 정의해 둔 컬렉터
Collectors 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 메서드를 제공 한다.
Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분되며, 앞으로 이 세 가지를 공부해볼 것이다.
스트림 요소를 하나의 값으로 리듀스 및 요약
리스트에서 총합을 구하는 등의 다양한 연산을 수행한다.
스트림 요소 그룹화
특정 키를 이용해 그룹화하거나 서브 그룹에 추가로 리듀싱 연산을 적용하는 등의 작업이 가능하다.
리듀싱과 요약
컬렉터는 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.
아래는 counting() 팩토리 메서드가 반환하는 컬렉터를 사용해 메뉴의 총 개수를 구하는 코드이다.
Copy import static java . util . stream . Collectors . * ;
long howManyDishes = menu . stream () . collect ( counting() );
long howManyDishes = menu . stream () . count (); //collect 생략 가능
최댓값, 최솟값 찾기
스트림 Collectors.maxBy, Collectors.minBy 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다
두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 입력 인자로 받는다.
Copy Comparator < Dish > dishCaloriesComparator = Comparator . comparingInt (Dish :: getCalories);
Optional < Dish > mostCaloriesDish = menu . stream () . collect ( maxBy(dishCaloriesComparator) );
요약 연산
리듀싱 기능을 사용해 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산
합계
Collectors.summingInt, summingDouble, summingLong 메서드를 통해 합계를 구하는 요약 연산을 수행하는 Collector를 얻을 수 있다.
Collectors.summingInt 메서드는 객체를 int로 매핑하는 함수를 인수로 받고, 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.
다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.
Copy int totalCalories = menu . stream () . collect ( summingInt(Dish :: getCalories) );
평균값 계산
Collectors.averagingInt, averagingLong, averagingDouble 정적 메서드를 통해 제공된다.
Copy double avgCalories = menu . stream () . collect ( averagingInt(Dish :: getCalories) );
합계, 평균, 최소값 및 최대값을 한번에 구하기
만약 두개 이상의 연산이 한번에 수행되어야 한다면 Collectors.summarizingInt 정적 메서드가 반환하는 컬렉터를 사용할 수 있다.
Copy IntSummaryStatistics menuStatistics = menu . stream () . collect ( summarizingInt(Dish :: getCalories) );
// menuStatistics : IntSummaryStatistics{count=9, sum=4300, min=120, average=477.778, max=800}
문자열 연결
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
내부적으로 StringBuilder를 이용해 문자열을 하나로 만든다.
joining 메서드의 인자로 구분자를 넣을 수 있다.
Copy String shortMenu = menu . stream () . map (Dish :: getName) . collect ( joining( ", " ) );
범용 리듀싱 요약 연산
reducing 팩토리 메서드를 사용해 지금까지 다룬 모든 컬렉터를 정의할 수도 있지만, 가독성과 편의성을 위해 특화된 컬렉터를 따로 제공하고 있다.
reducing 메서드의 입력 인자
리듀싱 연산의 시작 값 혹은 스트림이 비었을 때의 반환값
같은 종류의 두 항목을 더해 하나의 값으로 만드는 BinaryOperator이다.
Copy public static < T , U > Collector< T , ? , U > reducing( U identity ,
Function<? super T , ? extends U > mapper ,
BinaryOperator< U > op) {
return new CollectorImpl <>(
boxSupplier(identity) ,
(a , t) -> { a[ 0 ] = op . apply (a[ 0 ] , mapper . apply (t)); } ,
(a , b) -> { a[ 0 ] = op . apply (a[ 0 ] , b[ 0 ]); return a; } ,
a -> a[ 0 ] , CH_NOID);
}
다음은 리듀싱을 사용해 칼로리의 합계와 최대 칼로리를 가진 메뉴를 구하는 코드이다.
Copy int totalCalrories = menu . stream () . collect ( reducing( 0 , Dish :: getCalories , (i , j) -> i + j) ;
Optional < Dish > mostCaloriesDish =
menu . stream () . collect ( reducing(
(d1 , d2) -> d1 . getCalories() > d2 . getCalories() ? d1 : d2) );
collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계되었으나, reduce는 두 값을 하나로 도출하는 불변형 연산이다.
따라서 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 reduce 메서드 대신 collect 메서드를 사용해 리듀싱 연산을 구현해야 한다.
잘 이해가 가지 않으니 7장에 가서 자세히 알아보도록 하자...😓
자신의 상황에 맞는 최적의 해법 선택
함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있다.
컬렉터를 이용하면 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 코드가 복잡해지지만, 재사용성과 커스터마이즈 가능성을 제공하여 높은 수준의 추상화와 일반화를 얻을 수 있다.
모든 메뉴의 칼로리 합계를 구할 때 아래와 같이 다양한 방법을 사용할 수 있다.
Copy // 1) reducing 컬렉터와 Integer 클래스의 sum 메서드 참조를 사용
int totalCalories = menu . stream () . collect ( reducing( 0 , Dish :: getCalories , Integer :: sum) );
// 2) 스트림을 IntStream으로 매핑해 sum 메서드를 호출
int totalCalories = menu . stream () . mapToInt (Dish :: getCalories) . sum ();
그룹화
분류 함수
groupingBy 메서드의 인자로 전달되는 함수가 이에 해당
예를 들어 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수
그룹화 연산의 결과로는 "그룹화 함수가 반환하는 값을 Key로, 그리고 키에 해당하는 스트림 요소의 리스트를 Value로 갖는 맵"이 반환된다.
단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.
다음 코드는 400 칼로리 이하를 'diet', 400~700칼로리를 'normal', 700칼로리 이상을 'fat' 요리로 분류하여 Map 형태로 저장한다.
Copy public enum CaloricLevel { DIET , NORMAL , FAT }
Map < CaloricLevel , List < Dish >> dishesByCaloricLevel = menu . stream () . collect (
groupingBy(dish -> {
if ( dish . getCalories() <= 400 ) return CaloricLevel . DIET ;
else if ( dish . getCalories() <= 700 ) return CaloricLevel . NORMAL ;
else return CaloricLevel . FAT ;
}) );
그룹화된 요소 조작
필터링
그룹의 요소 중 특정 조건으로 필터링하려면 groupingBy() 팩토리 메서드를 오버로드해 두번째 인자로 filtering() 메서드를 넣어주어야 한다.
미리 조건으로 필터링한 후 groupingBy()를 수행하면 조건에 해당하지 않는 키가 아예 누락되는 문제가 있다.
Copy // 먼저 필터링할 경우 FISH는 미리 필터링되므로 키가 누락된다.
Map < Dish , Type , List < Dish >> caloricDishesByType = menu . stream ()
. filter (dish -> dish . getCalories () > 500 )
. collect ( groupingBy(Dish :: getType) );
// 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef]}
// groupingBy 내부에 filtering Predicate를 넣어 모든 키를 보존할 수 있다.
Map < Dish , Type , List < Dish >> caloricDishesByType = menu . stream ()
. collect ( groupingBy(Dish :: getType , filtering(dish -> getCalrories() > 500 , toList())) );
// 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
매핑
Copy // mapping Predicate 사용해 메뉴 이름만 리스트로 반환
Map < Dish . Type , List < Sting >> dishNamesByTypes = menu . stream ()
. collect ( groupingBy(Dish :: Type , mapping(Dish :: getName , toList())) );
Copy // flatMapping Predicate 사용해 dishTags Map에서 메뉴에 해당하는 태그 리스트들으로부터 Set을 반환
Map < Dish . Type , Set < String >> dishNamesByType = menu . stream ()
. collect ( groupingBy(Dish :: getType ,
flatMapping(dish -> dishTags . get( dish . getName()) . stream() , toSet())) );
// 결과: {MEAT=[salty, greasy, roasted, fried, crisp], FISH= [roasted, tasty, fresh], OTHER:[salty, natural, light]}
다수준 그룹화
여러 조건을 기준으로 이중, 삼중맵으로 그룹화할 수 있다.
groupingBy 메서드 내부에 두번째 기준을 정의하는 groupingBy 메서드를 전달해 이중맵으로 스트림의 항목을 그룹화할 수 있다.
보통 groupingBy의 연산을 '버킷(물건을 담을 수 있는 양동이)' 개념으로 생각하면 쉽다.
첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채우기를 반복하면 n수준의 그룹화가 가능하다.
다음 코드는 메뉴 종류에 따라 1차 분류한 후, 칼로리 정도에 따라 2차 분류하는 방식이다.
Copy Map < Dish . Type , Map < CalricLevel , List < Dish >>> dishesByTypeCaloricLevel = menu . stream ()
. collect (
groupingBy(Dish :: getType ,
groupingBy(dish -> {
if ( dish . getCalories() <= 400 ) return CaloricLevel . DIET ;
else if ( dish . getCalories() <= 700 ) return CaloricLevel . NORMAL ;
else return CaloricLevel . FAT ;
})
)
};
//dishesByTypeCaloricLevel :
//{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={...}}
서브 그룹으로 데이터 수집
분류 함수 한개만 인수를 갖는 groupingBy(f) 메서드는 사실 groupingBy(f, toList())의 축약형이다.
groupingBy 메서드는 두번째 인수로 다양한 컬렉터를 전달받을 수 있어, 이를 이용해 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 수 있다.
아래는 counting 컬렉터를 입력하는 코드이다.
Copy Map < Dish . Type , Long > typesCount = menu . stream () . collect ( groupingBy(Dish :: getType , counting()) );
// 결과: {MEAT=3, FISH=2, OTHER=4}
Collectors.collectingAndThen 팩토리 메서드는 컬렉터가 반환한 결과를 변환 함수에 적용시켜 다른 컬렉터를 반환한다.
아래와 같이 Optional.get()으로 Optional의 값을 꺼낼 수 있으며, 이 때 리듀싱컬렉터는 절대 Optional.empty()를 반환하지 않으므로 get()을 호출해도 안전하다.
Copy Map < Dish . Type , Optional < Dish >> mostCaloricByType = menu . stream ()
. collect ( groupingBy(Dish :: getType ,
collectingAndThen(maxBy(CompaingInt(Dish :: getCalories)) ,
Optional :: get)) );
// 결과: {FISH=salmon, OTHER=pizza, MEAT=pork}
여러 컬렉터 예제
summingInt 컬렉터를 사용해 메뉴 종류에 따른 칼로리의 합계를 반환한다.
Copy Map < Dish . Type , Integer > totalCaloriesByType = menu . stream ()
. collect ( groupingBy(Dish :: getType , summingInt(Dish :: getCalories)) );
mapping 컬렉터를 사용해 메뉴 종류에 따른 칼로리 분류 셋을 반환한다.
toCollection을 사용해 원하는 객체로 반환하도록 할 수 있다.
Copy Map < Dish . Type , Set < CaloricLevel >> caloricLevelsByType = menu . stream ()
. collect ( groupingBy(Dish :: getType , mapping(dish -> {
if ( dish . getCalories() <= 400 ) return caloricLevel . DIET ;
else if ( dish . getCalories() <= 700 ) return caloricLevel . NORMAL ;
else return caloricLevel . FAT ;
} , toCollection(HashSet ::new ))) );
//{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
분할
분할 함수를 분류 함수로 사용하는 특수한 그룹화 기능이다.
분할 함수(boolean을 반환하는 Predicate)를 분류 함수로 사용하기 때문에 결과 맵의 키 형식은 Boolean이다.
다음 코드는 채식 메뉴이면 true 그룹에, 채식 메뉴가 아니면 false 그룹에 들어가도록 한다.
Copy Map < Boolean , List < Dish >> partitionedMenu = menu . stream ()
. collect ( partitioningBy(Dish :: isVegetarian) );
// {false=[pork, beef, chicken, prawns, salmon], true=[french fires, rice, season fruit, pizza]}
장점
분할 함수가 반환하는 참, 거짓의 스트림 리스트를 모두 유지한다.
partitioningBy 메서드의 두번째 인자로 컬렉터를 전달할 수도 있다.
Copy // groupingBy 컬렉터를 전달해 타입별로 그룹핑
Map < Boolean , List < Dish >> partitionedMenu = menu . stream () . collect ( partitioningBy(
Dish :: isVegetarian , groupingBy(Dish :: getType)) ;
// 결과: {false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fires, rice, season fruit, pizza]}}
// maxBy 컬렉터를 전달해 칼로리가 가장 높은 메뉴만 반환
Map < Boolean , Dish > partitionedMenu = menu . stream () . collect ( partitioningBy(
Dish :: isVegetarian , collectingAndThen(maxBy(comparingInt(Dish :: getCalories)) , Optional :: get)) ;
// 결과: {false=pork, true=pizza}
partitioningBy 메서드의 두번째 인자로 partitioningBy 메서드를 중첩할 수도 있다.
Copy Map < Boolean , Map < Boolean , List < Dish >>> partitionedMenu =
menu . stream ()
. collect ( partitioningBy(Dish :: isVegetarian ,
partitioningBy(d -> d . getCalories() > 500 )) );
소수와 비소수로 분할
정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누어보자.
소수이면 true 그룹에, 비소수이면 false 그룹에 들어가도록 한다.
먼저 소수인지 판별하는 메서드부터 만든다. (특정 수의 제곱근보다 작은 수로 특정 수를 나눌 수 없으면 소수이다.)
Copy public boolean isPrime( int candidate) {
int candidateRoot = ( int ) Math . sqrt (( double )candidate);
return IntStream . rangeClosed ( 2 , candidateRoot) . noneMatch (i -> candidate % i == 0 );
}
public Map< Boolean , List< Integer >> partitionPrimes( int n) {
return IntStream . rangeClosed ( 2 , n)
. boxed ()
. collect ( partitioningBy(candidate -> isPrime(candidate)) );
}
Collector 인터페이스
Collector 인터페이스는 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
메서드 종류
Copy public interface Collector < T , A , R > {
Supplier < A > supplier ();
BiConsumer < A , T > accumulator ();
Function < A , R > finisher ();
BinaryOperator < A > combiner ();
Set < Characteristics > characteristics ();
}
위 코드는 Collector 인터페이스의 5가지 메서드를 나타낸다.
supplier : 새로운 결과 컨테이너를 만든다.
accumulator : 결과 컨테이너에 요소를 추가한다.
finisher : 최종 변환값을 결과 컨테이너로 적용한다.
combiner : 두 결과 컨테이너를 병합한다.
characteristics : collect 메서드가 어떤 최적화를 이용해 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공한다.
A는 누적자. 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
R은 수집 연산 결과 객체의 형식(보통 컬렉션 형식)이다.
supplier
새로운 결과 컨테이너를 만들기 위해 빈 결과로 이루어진 Supplier를 반환해야 한다.
supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수이다.
ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
Copy public Supplier<List< T >> supplier() {
return ArrayList ::new ;
}
accumulator
스트림에서 n번째 요소를 탐색할 때, n-1번째 항목까지 수집한 누적자와 n번째 요소를 함수에 적용한다.
함수의 반환값은 void이고 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로, 누적자가 어떤 값일지 단정할 수 없다.
ToListCollector에서 accumulator가 반환하는 함수는 기존 리스트(누적자)에 현재 항목을 추가한다.
Copy public BiConsumer<List< T > , T > accumulator() {
return List :: add;
}
finisher
스트림 탐색을 끝내고 누적자 객체를 최종 객체로 변환할 때 호출할 함수를 반환해야 한다.
ToListCollector는 누적자 객체가 최종 결과이기 때문에 변환 과정이 필요없어 항등 함수를 반환한다.
Copy public Function<List< T > List< T >> finisher() {
return Function . identity ();
}
combiner
스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
toList의 combiner는 간단하게 스트림의 두 번째 서브 파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
Copy public BinaryOperator<List< T >> combiner() {
return (list1 , list2) -> {
list1 . addAll (list2);
return list1;
}
}
스트림의 리듀싱을 병렬로 처리할 수 있도록 해주는 메서드이다.
병렬 리듀싱 수행 과정
스트림을 언제까지 분할할지에 대한 조건에 맞추어 스트림을 재귀적으로 분할해 서브스트림들을 만든다.
모든 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용해 병렬로 처리한다.
컬렉터의 combiner 메서드가 반환하는 함수를 사용해 서브스트림의 결과들을 합친다.
Characteristics
컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
Characteristics는 스트림을 병렬로 리듀스 할 지 여부와, 병렬로 리듀스 시 선택할 최적화에 대한 힌트를 제공한다.
Characteristics의 항목
UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
CONCURRENT : 다중 스레드에서 accumulator 함수를 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.
IDENTITY_FINISH : 최종 결과로 누적자 객체를 바로 사용하는 경우 finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
정리
아래와 같이 ToListCollector를 구현할 수 있다.
Copy public class ToListCollector < T > implements Collect < T , List < T > , List < T >> {
@ Override
public Supplier < List < T >> supplier () {
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) -> {
liat . addAll (list2);
return list1;
}
}
@ Override
public Set < Characteristics > characteristics () {
return Collections . unmodifiableSet ( EnumSet . of (IDENTITY_FINISH , CONCURRENT));
}
}
컬렉터 구현 클래스를 만드는 대신 세 함수(발행, 누적, 합침)를 인수로 받는 collect메서드를 사용할 수 있다.
단, IDENTITY_FINISH 수집 연산일 때 해당된다.
세 함수는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.
Copy List < Dish > dishes = menuStream . collect (
ArrayList ::new , //발행
List :: add , //누적
List : addAll); //합침
커스텀 컬렉터
어떤 수가 소수인지 판별하기 위해서는, 소수로만 나누어떨어지는지 확인하면 된다.
일반적으로는 컬렉터 수집 과정에서 부분 결과에 접근할 수 없으므로 커스텀 컬렉터 클래스를 통해 접근할 수 있다.
아래 isPrime메서드에서는 primes 리스트를 입력받도록 했다.
대상의 제곱근보다 큰 소수를 찾으면 검사를 중단하기 위해서는 takeWhile 메서드를 사용한다.
Copy public boolean isPrime( List< Integer > primes , int candidate) {
int candidateRoot = ( int ) Math . sqrt (( double )candidate);
return primes . stream ()
. takeWhile (i -> i <= candidateRoot);
. noneMatch (i -> candidate % i == 0 );
}
커스텀 컬렉터 만들기
public interface Collector<T, A, R>
에서 T는 스트림 요소의 형식, A는 중간 결과 누적 객체의 형식, R은 collect 연산의 최종 결과 형식을 의미한다.
우리가 만드는 컬렉터는 소수는 key가 true인 리스트에 저장하고, 비소수는 key가 false인 리스트에 저장한다.
Copy public class PrimeNumbersCollector implements Collect < Integer , Map < Boolean , List < Integer >> ,
Map < Boolean , List < Integer >>> {
// 1) supplier: 누적자 초기화하는 함수 반환
public Supplier < Map < Boolean , List < Integer >>> supplier () {
return () -> new HashMap < Boolean , List < Integer >>() {{
put( true , new ArrayList < Integer >()) ;
put( false , new ArrayList < Integer >()) ;
}};
}
// 2) accmulator: 누적자에 요소 추가하는 함수 반환
public BiConsumer < Map < Boolean , List < Integer >> , Integer > accumulator () {
return ( Map< Boolean , List< Integer >> acc , Integer candidate) -> {
acc . get ( isPrime( acc . get( true ) , candidate) ) // 현재까지의 소수 리스트를 isPrime에 넘긴다.
. add (candidate);
};
}
// 3) combiner: 병렬 수행으로 여러 누적자가 생성될 때 합쳐주는 함수 반환
public BinaryOperator < Map < Boolean , List < Integer >>> combiner () {
return ( Map< Boolean , List< Integer >> map1 , Map< Boolean , List< Integer >> map2) -> {
map1 . get ( true ) . addAll ( map2 . get ( true ));
map1 . get ( false ) . addAll ( map2 . get ( false ));
};
}
// 4) finisher: 누적자와 결과 형식이 동일하므로 항등함수 반환
public Function < Map < Boolean , List < Integer >> , Map < Boolean , List < Integer >>> finisher () {
return Function . identity ();
}
// 5) 커스텀 컬렉터는 CONCURRENT, UNORDERED에는 해당하지 않는다.
public Set < Characteristics > characteristics () {
return Collections . unmodifiableSet ( EnumSet . of (IDENTITY_FINISH));
}
}
커스텀 컬렉터를 아래와 같이 사용할 수 있다.
Copy public Map< Boolean , List< Integer >> partitionPrimes( int n) {
return IntStream . rangeClosed ( 2 , n)
. boxed ()
. collect ( new PrimeNumbersCollector() );
}
혹은 collect 메서드 인자로 supplier, accumulator, combiner 함수를 직접 넣어 사용할 수도 있다. (가독성과 재사용성이 떨어지기 때문에 코드는 생략하겠습니다 🙂)