collect
메서드에 Collector
인터페이스 구현을 전달한다
스트림에 collect
를 호출하면 스트림의 요소에 리듀싱 연산이 수행됨
collect
에서 리듀싱 연산을 이용해 스트림의 각 요소를 방문하면서 Collector
가 작업 처리Collector에서 제공하는 메서드의 기능
요약연산: 스트림에 있는 객체의 숫자 필드의 합계/평균 등 반환하는 연산
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream()
.collect(maxBy(dishCaloriesComparator));
Collectors.summingInt
(Collectors.summingLong
, Collectors.summingDouble
)
int
로 매핑하는 함수를 인수로 받음int
로 매핑한 컬렉터 반환summingInt
가 collect
메서드로 전달되면 요약 작업 수행int totalCalories = menu.stream()
.collect(summingInt(Dish::getCalories));
averagingInt
, averagingLong
, averagingDouble
summarizingInt
, summarizingLong
, summarizingDouble
컬렉터에 joining
팩토리 메서드 사용하면 스트림의 각 객체에 toString
메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환함
joining
메서드는 내부적으로 StringBuilder
를 사용해서 문자열을 하나로 만듬joining
의 인수로 문자열을 넘기면 각 객체의 toString
결과값 사이에 문자열 끼워넣기 가능String shortMenu = menu.stream().map(Dish::getName).collect(joining(" "));
지금까지 쓰인 모든 컬렉터는 reducing
팩토리 메서드로도 정의할 수 있음
Collectors.reducing
으로 구현 가능int totalCalores = menu.stream()
.collect(
reducing(0, Dish::getCalories, (i, j) -> i + j)
);
reducing
의 인자 3개
1. 첫번째 인수: 리듀싱 연산의 시작값. 스트림에 인수가 없을 때는 반환값
2. 두번째 인수: 요리 -> 칼로리값 변환 함수
3. 세번째 인수: 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator
int totalCalories = menu.stream()
.collect(
reducing(0, Dish::getCalories, Integer::sum)
);
위 코드와 아래 코드는 같은 값을 반환함
int totalCalories = menu.stream()
.map(Dish::getCalories)
.reduce(Integer::sum)
.get()
아래 코드도 같은 값을 반환함
int totalCalories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
분류함수(groupingBy
): 스트림이 그룹화되는 기준이 되는 함수
Map<Dish.Type, List<Dish>> dishesByType
= menu.stream()
.collect(groupingBy(Dish::getType));
filter
와 collect
를 같이 사용해 필터링 조건과 그룹화를 함께 수행할 때, 두 함수를 순차적으로 사용하면 필터링해서 모든 값이 걸러진 키는 결과에 존재도 안하게 된다. 주로 프로그래머가 원하는 것은 키는 존재하지만 값은 존재하지 않는 것.
이 때는 두 함수를 합쳐 사용하면 된다.
예를 들어, 이렇게 코드를 짜고 싶으면,
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
// {Other=[french fries, pizza], MEAT=[pork, beef]}
아래처럼 코드를 짜면 된다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(
groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500,
toList()
)
);
// {Other=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
flatmapping
을 활용해서 간편하게 추출할 수도 있다
Map<Dish.Type, Set<String>> dishNamesByType =
menu.stream()
.collect(
groupingBy(
Dish::getType,
flatMapping(dish -> dishTags.get(dish.getName())
.stream(), toSet()
)
)
);
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.NORAML;
else return CaloricLevel.FAT;
})
)
);
// MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
// Fish={DIET=[prawns], NORMAL=[salmon]}
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(
groupingBy(
Dish::getType, // 분류 함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get // 변환 함수
)
)
);
collectingAndThen
: 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환하는 팩토리 메서드collect
의 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑maxBy
로 만들어진 컬렉터가 감싸지는 컬렉터. 변환 함수 Optional::get
으로 반환된 Optional
에 포함된 값 추출분할 함수: predicate을 분류 함수로 사용하는 특수한 그룹화 기능
boolean
반환함Boolean
<Map<Boolean, List<Dish>> partitionedMenu =
menu.stream()
.collect(
partitioningBy(Dish::isVegeterian)
);
// {false = [pork, beef], true[french fries, rice, pizza]}
분할의 장점: 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지함
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream()
.collect(
partitioningBy(Dish::isVegeterian, groupingBy(Dish::getType))
);
// {false={FISH=[prawns, salmon], MEAT=[pork, beef]},
// true={OTHER=[french fries, rice, pizza]}}
채식 요리와 채식이 아닌 요리 각각의 그룹에서 가장 칼로리가 높은 요리 찾기
Map<Boolean, Dish> mostCaloricPartitionedByVegeterian =
menu.stream()
.collect(
partitioningBy(Dish::isVegeterian,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
// {false=pork, true=pizza}
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
T
- 수집될 스트림 항목의 제네릭 형식A
- 누적자. 수집 과정에서 중간 결과를 누적하는 객체의 형식R
- 수집 연산 결과 객체의 형식supplier
메서드: 빈 결과로 만들어진 Supplier
반환
accumulator
메서드 - 리듀싱 연산을 수행하는 함수 반환
n
번째 요소를 탐색할 때 두 인수, 즉 누적자와 n
번째 요소를 함수에 적용finisher
메서드: 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환함
ToListCollector
등을 사용하면 누적자 객체가 이미 최종 결과인 상황도 존재 > 이 때는 변환 과정이 필요 없어서 finisher
메서드는 항등 함수 반환combiner
메서드: 리듀싱 연산에서 사용할 함수를 반환함
characteristics
메서드: 컬렉터의 연산을 정의하는 Characteristics
형식의 불변 집합 반환
Characteristics
UNORDERED
- 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않음CONCURRENT
accumulator
함수를 동시에 호출할 수 있음UNORDERED
를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있음IDENTITY_FINISH
finisher
메서드가 반환하는 함수는 단순히 identity
를 적용할 뿐이므로 이를 생략 가능참고: Modern Java in Action (라울-게이브리얼 등 지음)