스트림 최종 연산 reduce가 그랬던 것처럼 collect 역시 다양한 요소 누적 방식을 인수로 받아서 스트림을 최종 결과로 도출하는 리듀싱 연산을 수행할 수 있다.
다양한 요소 누적 방식은 Collector 인터페이스에 정의되어 있다.
6.1 컬렉터란 무엇인가?
Collectors 클래스의 toList 메서드는 스트림의 각 요소를 리스트로 만드는 작업을 수행하는 Collector 인터페이스의 구현을 반환한다.
collect 메서드의 인수로 주어진 Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할 지 결정한다.
Collectors 클래스의 groupingBy 메서드는 각 키 버킷 그리고 각 키 버킷에 대응하는 요소 리스트를 값으로 포함하는 맵을 만드는 작업을 수행하는 Collector 인터페이스의 구현을 반환한다.
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다
collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 작업을 처리한다.
특정 데이터 별로 스트림 요소를 그룹화하는 리듀싱 연산
Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할 지 결정된다.
Collectors 유틸리티 클래스는 자주 사용되는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.(ex. Collectors.toList())
6.1.2 미리 정의된 컬렉터
Collectors에서 제공하는 메서드의 기능 3가지
스트림 요소를 하나의 값으로 리듀스하고 요약
요소 그룹화
요소 분할
6.2 리듀싱과 요약
Collector 인터페이스 구현체로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.
counting() 팩토리 메서드로 생성한 컬렉터
import static java.util.stream.Collectors.*;
long howManyDishes = menu.stream().collect(counting());
6.2.1 스트림값에서 최댓값과 최솟값 검색
maxBy, minBy 두 개의 팩토리 메서드로 생성한 컬렉터로 스트림의 최댓값과 최솟값을 구할 수 있다.
import static java.util.stream.Collectors.*;
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
6.2.2 요약 연산
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories))
averagingInt, averagingLong, averagingDouble 등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.
double avgCalories =
menu.stream().collect(averagingInt(Dish::getCalories));
팩토리 메서드 summarizingInt가 반환하는 컬렉터로 스트림의 요소 수, 합계와 평균, 최솟값과 최댓값을 한 번에 계산할 수 있다
IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
// menuStatistics 객체를 출력하면
// IntSummaryStatistics{count=9, sum=4300, min=120, average=477.77778, max=800}과 같은 정보 출력
6.2.3 문자열 연결
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
결과에 구분자를 추가하기 위해 joining 팩토리 메서드에 인자로 구분자 전달
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
6.2.4 범용 리듀싱 요약 연산
지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다.
int total Calories = menu.stream()
.collect(reducing(
0, Dish::getCalories, (i, j) -> i + j));
예제의 reducing은 인수 세 개를 받는다.
첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.
두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수
세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator
한 개의 인수를 갖는 reducing 팩토리 메서드는 세 개의 인수를 갖는 reducing 팩토리 메서드에서 스트림의 첫 번째 요소를 시작 요소, 즉 첫 번째 인수로 받으며, 자신을 그대로 반환하는 항등 함수를 두 번째 인수로 받는 상황에 해당
한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌을 때 시작값이 설정되지 않는 상황이 발생한다. 그래서 reducing 컬렉터는 Optional< T > 타입의 객체를 반환
Optional<Dish> mostCaloriesDish =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
collect와 reduce
Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();
List<Integer> numbers = stream.reduce(
new ArrayList<Integer>(),
(List<Integer> l, Integer e) -> {
l.add(e);
return l; },
(List<Integer> l1, List<Integer> l2) -> {
l1.add(l2);
return l1; });
위 코드에는 의미론적인 문제와 실용적인 문제가 발생한다.
collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드지만 reduce 메서드는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적으로 문제
위 예제의 reduce 메서드는 누적자로 사용된 리스트를 변환시키므로 reduce를 잘못 활용한 예이다.
여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 실용성 문제 => 매번 새로운 리스트를 할당해야하므로 객체를 할당하느라 성능 저하
가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직하다.
6.3 그룹화
팩토리 메서드 groupingBy를 이용한 그룹화로 스트림의 항목을 분류하는 과정
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
// dishesByType Map에 포함된 결과
{FISH=[prawns, salmon], OTHER=[french fries, rice, season fruit, pizza],
MEAT=[pork, beef, chicken]}
6.3.1 그룹화된 요소 조작
Collectors 클래스의 filtering 메서드
요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작 하는 연산이 필요하다.
만약 500 칼로리가 넘는 요리만 필터한다고 가정하면 그룹화하기 전에 프레디케이트로 필터를 적용해 문제를 해결할 수 있다고 생각할 수 있다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream().filter(dish->dish.getCaloric() > 500)
.collect(groupingBy(Dish::getType));
// 그룹화하기 전에 프레디케이트로 필터를 적용한 결과
{OTHER=[french fries, pizza], MEAT=[pork, beef]} // FISH 그룹이 결과에서 사라짐
그룹화하기 전에 프레티케이트로 먼저 필터를 적용하면 필터를 만족하지 않는 그룹은 결과에서 배제되는 문제가 발생한다.
Collectors 클래스는 일반적인 분류 함수에 Collector 형식의 구현을 두 번째 인수를 갖도록 groupingBy 팩토리 메서드를 오버로드해 이 문제를 해결한다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(
Dish::getType,
filtering(dish->dish.getCaloric() > 500, toList())
));
Collectors 클래스의 filtering 메서드는 Collectors 클래스의 또 다른 정적 팩토리 메서드로 프레디케이트를 인수로 받아 각 그룹의 요소와 필터링된 요소를 재그룹화한다.
// filtering 메서드로 각 그룹의 요소와 필터링된 요소를 재그룹화한 결과
{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
Collectors 클래스의 mapping 메서드
그룹화된 항목을 조작하는 다른 유용한 기능 중 하나로 맵핑 함수를 이용해 요소를 변환하는 작업이 있다.
Collectors 클래스는 매핑 함수와 각 항목에 적용한 함수를 모으는 데 사용하는 또 다른 컬렉터를 인수로 받는 mapping 메서드를 제공한다.
Map<Dish.Type, List<String>> dishNamesByType =
menu.stream()
.collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
Collectors 클래스의 flatMapping 메서드
메뉴 그룹에 속한 메뉴에 달린 태그들이 메뉴 그룹과 매핑되는 Map을 만들어보자
우선 다음과 같이 각 메뉴의 이름에 대해 태그 리스트를 매핑시킨 맵이 있다고 하자
Map<String, List<String>> dishTags = new HashMap<>();
dishTags.put("pork", asList("salty", "greasy"));
dishTags.put("beef", asList("salty", "roasted"));
dishTags.put("chicken", asList("fried", "crisp"));
dishTags.put("french fries", asList("greasy", "fried"));
dishTags.put("rice", asList("light", "natural"));
dishTags.put("season fruit", asList("fresh", "natural"));
dishTags.put("pizza", asList("tasty", "salty"));
dishTags.put("prawns", asList("greasy", "roasted"));
dishTags.put("salmon", asList("delicious", "fresh"));
grouping 메서드를 통해 얻은 Collector 인터페이스의 grouping 컬렉터를 사용하면 각 메뉴들이 자신이 속한 메뉴 그룹으로 그룹화가 될 것이다
그 다음 각 메뉴 그룹에 속한 메뉴들로부터 태그들을 얻어내는 연산을 해야하는데, 이 때 Collectors 클래스의 mapping 메서드를 이용하면, 각 메뉴 그룹에 속한 각 메뉴들에 대해서 각 메뉴마다 그것에 달린 태그가 아닌 태그 '리스트'를 얻어오게 된다.
즉, mapping 메서드를 이용해 재그룹화한 결과를 보면, 메뉴 그룹마다 대응되는 요소가 그 메뉴 그룹에 속한 메뉴들에 달린 태그들이 아닌, 태그 '리스트'들이 된다는 것이다.
이러한 문제를 해결하기 위해선 mapping 메서드를 이용하는 것이 아닌, 각 그룹마다 그 그룹에 속하는 각 메뉴들에 각각 매핑된 태그 리스트들을 그 그룹에 대응되는 하나의 리스트로 추리는, 즉 하나의 리스트로 평면화하는 flatMapping 메서드를 이용해야 한다.
추가로 태그들이 중복되는 것을 방지하기 위해서 재그룹화를 위한 flatMapping 연산 결과를 담는 저장 공간은 Set 형식으로 한다. 집합으로 그룹화해 중복 태그를 제거해야 한다.
Map<Dish.Type, Set<String>> dishTagsByType =
menu.stream()
.collect(
groupingBy(
Dish::getType,
flatMapping(dish->dishTags.get(dish.getName()).stream(),
// dishTags.get(dish.getName())은 태그 리스트 반환환
// 매핑된 리스트들을 스트림 평면화
toSet())
)
);
6.3.2 다수준 그룹화
한 기준이 아닌 두 가지 이상의 기준을 동시에 적용해 리스트를 그룹화해보자
두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화할 수 있다.
Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받는다.
즉, 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목을 그룹화할 수 있다.
Map<Dish.Type, Map<CaloricLevel, 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;
})
)
);
{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
FISH={NORMAL=[salmon], DIET=[prawns]},
OTHER={NORMAL=[pizza, fries], DIET=[fruit,rice]}}
6.3.3 서브그룹으로 데이터 수집
사실 첫 번째 groupingBy로 넘겨주는 컬렉터의 형식은 제한이 없다.
분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 축약형이다.
Map<Dish.Type, Long> typesCount =
menu.stream().collect(
groupingBy(Dish::getType, counting()) // counting() => 개수 수집 컬렉터
);
// {MEAT=3, FISH=2, OTHER=4}
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(
groupingBy(
Dish::getType,
maxBy(comparingInt(Dish::getCalories))
)
);
// {FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
Optional< Dish >는 해당 종류의 음식 중 가장 높은 칼로리를 래핑한다.
처음부터 존재하지 않는 요리의 키는 맵에 추가되지 않기 때문에 실제로 메뉴의 요리 중 Optional.empty() 를 값으로 갖는 요리는 존재하지 않는다.
마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있다.
컬렉터 결과를 다른 형식에 적용하기
Collectors.collectingAndThen으로 컬렉터가 반환한 결과를 다른 형식으로 활용 가능
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(
groupingBy(Dish::getType, // 분류 함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get // 변환 함수
)
)
)
groupingBy와 함께 사용하는 다른 컬렉터 예제
일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때는 팩토리 메서드 groupingBy 메서드에 두 번째 인수로 전달한 컬렉터를 사용한다.
Map<Dish.Type, Integer> totalCalorieByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish:getCalories) // summing 리듀싱 연산
));
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;
}, toSet() )));
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) )));
6.4 분할
분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.
분할 함수는 불리언을 반환하므로 그룹화 결과 맵의 키 형식은 Boolean이다.
결과적으로 그룹화 맵은 참 아니면 거짓의 값을 갖는 최대 두 개의 그룹으로 분류된다
Menu<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(
Dish::isVegetarian
));
{false=[pork, beef, chicken, prawns, salmon],
true=[french fries, rice, season fruit, pizza]}
6.4.1 분할의 장점
분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할 함수의 장점이다.
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().
collect(
partitioningBy(
Dish:isVegetarian,
groupingBy(
Dish::getType
)
)
);
{false=[FISH=[prawns, salmon], MEAT=[pork, beef, chicken]],
true=[OTHER=[french fries, rice, season fruit, pizza]
}
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
partitioningBy(
Dish::isVegetarian,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get
)
)
);
// {false=pork, true=pizza}
6.4.2 숫자를 소수와 비소수로 분할하기
정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누는 프로그램
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)));
}
Collectors 클래스의 정적 팩토리 메서드
팩토리 메서드
반환 형식
사용 예제
활용 예
toList
List < T >
스트림의 모든 항목을 리스트로 수집
List< Dish > dishes = menuStream.collect(toList());
toSet
Set < T >
스트림의 모든 항복을 중복이 없는 집합으로 수집
Set< Dish > dishes = menuStream.collect(toSet());
toCollection
Collection < T >
스트림의 모든 항목을 발행자가 제공하는 컬렉션으로 수집
Collection< Dish> dishes = menuStream.collect(toCollection(), ArrayList::new);
counting
Long
스트림의 항목 수 계산
long howManyDishes = menuStream.collect(counting());
summingInt
Integer
스트림의 항목에서 정수 프로퍼티 값을 더함
int totalCalories = menuStream.collect(summingInt( Dish::getCalories));
avaragingInt
Double
스트림 항목의 정수 프로퍼티의 평균값 계산
double avgCalories = menuStream.collect(averaginInt( Dish::getCalories));
summarizingInt
IntSummaryStatistics
스트림 내 항목의 최댓값, 최솟값, 합계, 평균 등의 정수 정보 통계 수집
IntSummaryStatistics = menuStream.collect( summarizingInt( Dish::getCalories));
joining
String
스트림의 각 항목에 toString 메서드를 호출한 결과 문자열 연결
String shortMenu = menuStream.map(Dish::getName) .collect( joining(", "));
maxBy
Optional< T >
주어진 비교자를 이용, 스트림의 최댓값 요소를 Optional로 감싼 값을 반환, 스트림에 요소가 없을 때는 Optional.empty() 반환
Optional< Dish > fattestMenu = menuStream.collect( maxBy(comparingInt( Dish::getCalories )));
minBy
Optional< T >
주어진 비교자를 이용, 스트림의 최솟값 요소를 Optional로 감싼 값을 반환, 스트림에 요소가 없을 때는 Optional.empty() 반환
Optional< Dish > fattestMenu = menuStream.collect( minBy(comparingInt( Dish::getCalories )));
reducing
리듀싱 연산 타입에 따른다
누적자를 초기값으로 설정, 스트림의 각 요소를 BinaryOperator로 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱
int totalCalories = menuStream.collect( reducing( 0, Dish::getCalories, Integer::sum));
collectingAndThen
변환 함수의 반환 타입에 따른다
다른 컬렉터를 감싸고 그 결과에 변환 함수 적용
int howManyDishes = menuStream.collect( collectingAndThen( toList(), List::size ) );
groupingBy
Map< K, List < T >>
하나의 프로퍼티 값을 기준으로 스트림의 항목을 그룹화, 기준 프로퍼티 값을 맵의 키로 사용
Map< Dish.Type, List< Dish >> dishesByType = menuStream.collect( groupingBy(Dish::getType) );
partitioningBy
Map <Boolean, List< T>>
프레디케이트를 스트림의 각 항목에 적용한 결과로 항목 분할
Map< Boolean, List< Dish >> vegetarianDishes = menuStream.collect( partitioningBy( Dish::isVegetarian));
6.5.1 Collector 인터페이스의 메서드 살펴보기
supplier 메서드: 새로운 결과 컨테이너 만들기
supplier 메서드는 빈 결과로 이루어지는 Supplier를 반환해야 한다.
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
accumulator 메서드: 결과 컨테이너에 요소 추가하기
accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다.
public BiConsumer<List<T>, T> accumulator(){
return List::add;
}
finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기
finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.
때로는 누적자 객체가 이미 최종 결과인 상황도 있다.
이런 때는 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환한다.
public Function<List<T>, List<T>> finisher(){
return Function.identity
}
순차 리듀싱 과정의 논리적 순서
combiner 메서드 : 두 결과 컨테이너 병합
combiner 매서드는 스트림의 서로 다른 서브 파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할 지 정의한다.
toList의 combiner의 경우, 스트림의 두 번째 서브 파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
컬렉터의 combiner 메서드를 이용하면 스트림의 리듀싱을 병렬로 수행할 수 있다.
위의 도식은 스트림의 병렬 리듀싱 수행 과정을 보여준다.
스트림을 분할해야 하는지 정의하는 조건이 거짓으로 바뀌기 전까지 스트림을 재귀적으로 분할한다.(분산된 작업의 크기가 너무 작아지면 병렬 수행 속도는 순차 속도보다 느려질 수 있다. 프로세싱 코어의 개수를 초과하는 병렬 작업은 효율적이지 않다.)
모든 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용해서 서브스트림을 병렬로 처리할 수 있다.
마지막에는 컬렉터의 combiner 메서드가 반환하는 함수로 모든 부분결과를 쌍으로 합친다. 분할된 모든 서브스트림의 결과를 합치면서 연산이 완료되는 것이다.
characteristics 메서드
characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 정의한다.
Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
Characteristics 열거형이다.
UNORDERED : 리듀싱 결과는 스트림의 요소 방문 순서나 누적 순서에 영향을 받지 않는다.
CONCURRENT: 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며, 해당 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 다만 컬렉터 플래그에 UNORDERED를 함께 설정하지 않았다면 집합처럼 데이터 소스가 정렬되어 있지 않은 상황에서만(순서가 무의미한 상황에서만) 병렬 리듀싱을 수행할 수 있다.
IDENTITY_FINISH: finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
6.5.2 응용하기
지금까지의 다섯 가지 메서를 이용해서 커스텀 ToListCollector를 구현
import java.util.*;
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 ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2)->{
list1.addAll(list2);
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return
Collections.unmodifiableSet
(EnumSet.of(Characteristics.IDENTITY_FINISH,
Characteristics.CONCURRENT));
}
}
List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>());
컬렉터 구현을 만들지 않고도 커스텀 수집 수행
Stream은 세 함수(발행, 누적, 합침(combine))를 인수로 받는 collect 메서드를 오버로드하며, 이 때의 인수로 주어지는 각각의 메서드는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.
List<Dish> dishes = memuStream.collect(
ArrayList::new, // 발행
List::add, // 누적
List::addAll // 합침
);
요약
collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법(컬렉터 인터페이스의 구현체)을 인수로 갖는 최종 연산이다.
스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최솟값, 최댓값, 평균값을 계산하는 컬렉터 등이 Collector 클래스 안의 팩토리를 통해 이미 정의되어 있다.
미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할할 수 있다.
컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.
Collector 인터페이스에 정의된 메서드(supplier, accumulator, combiner, finisher, characteristics)를 구현해서 커스텀 컬렉터를 개발할 수 있다.