이 장의 내용
collect와 컬렉터로 구현할 수 있는 질의 예제
통화별로 트랜잭션을 그룹화한 다음 해당 통화로 일어난 모든 트랜잭션 합계를 계산 → return Map<Currency, Integer>
트랜잭션을 비싼/저렴한 트랜잭션으로 분류 → return Map<Boolean, List<Transaction>>
트랜잭션을 도시 등 다수준으로 그룹화, 각 트랜잭션이 비싼/저렴한 구분 → return Map<String, Map<boolean, List<Transaction>>>
예시 코드
// 그룹화한 트랜잭션을 저장할 맵 생성
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
// 트랜잭션 리스트 반복
for(Transaction transaction : transactions) {
// 트랜잭션 통화 추출
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
// 현재 통화를 그룹화하는 맵에 항목이 없으면 항목을 만듦
if(transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>();
transactionsForCurrencies.put(currency, transactionsForCurrency);
}
// 같은 통화를 가진 트랜잭션 리스트에 현재 탐색 중인 트랜잭션을 추가
transactionsForCurrency.add(transaction);
}
// 스트림을 활용해 같은 예제 간단하게 구현 가능
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream().collect(groupingBy(Transaction::getCurrency));
컬렉터란
collect() 메서드를 이용해 Collector 인터페이스의 구현체를 전달
보통 collect(toList())메서드를 통해 Collector 인터페이스 구현제를 사용해 각 요소를 리스트로 만들도록 사용
고급 리듀싱을 수행하는 컬렉터

collect 메서드의 내부적 리듀싱 연산
collect() 메서드를 호출하면 스트림의 요소에 리듀싱 연산이 수행됨미리 정의된 컬렉터
groupingBy 처럼 미리 정의된 메서드 제공count(), sum() 등groupingBy() 등리듀싱과 요약
counting 활용
long howManyDishes = menu.stream().collect(Collectors.counting());
// 아래와 같이 간단하게 요약 가능
// import static java.util.stream.Collectors.* 했다고 가정
long howManyDishes = menu.straem().count();
최댓갑과 최솟값 검색
Collectors.maxBy, Collectors.minBy 메서드를 이용해 최솟값/최댓값 계산 가능
두 컬렉터는 비교하는 데 사용할 Comparator를 인수로 받음
예시 코드
// 가장 높은 칼로리 메뉴를 찾는 예시 코드
Comarator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator));
요약 연산

summingInt 계산 과정
합계, 평균 등을 반환하는 데에도 리듀싱 연산 기능이 사용 → 요약 연산이라고 부름
Collectors.summingInt 메서드는 객체를 int로 매핑하는 함수를 인수로 받아 해당 값을 모두 더해서 반환
summingDouble도 있는데 double에 대해 처리한다는 점만 다름
예시 코드


// 메뉴의 각 칼로리를 더해서 반환
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
// 메뉴의 칼로리 평균 반환
double avgCalories =
menu.stream().collect(averagingInt(Dish::getCalories));
// 만약 하나의 연산으로 합계, 평균을 구해야 하는 경우 종합 결과를 반환
IntSummaryStatistics menuStatistics =
menu.stream().collect(summarizingInt(Dish::getCalories));
// result -> IntSummaryStatistics{count=9, sum=4300, min=120, ...}
문자열 연결
컬렉터의 joining() 메서드를 이용하면 각 객체에 toString() 메서드 호출하여 추출한 문자열을 하나의 문자열로 연결해서 출력
joining() 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만듦
예시 코드
// 구분자 없이 연결
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
// 구분자를 사용한 연결
String shotMenu = menu.stream().map(Dish::getName).collect(joining(", "));
범용 리듀싱 요약 연산
지금까지의 내용은 reducing() 메서드로도 정의 가능
앞선 특화된 컬렉터를 사용하는 이유는 편의성 때문(가독성도 중요)
reduce와 reducing 차이 찾아보기 → reduce 사용하면 누적자로 사용된 리스트 변환?
예시 코드
// reducing을 활용한 모든 메뉴 칼로리의 합 계산
// 첫 번쨰 인수는 초깃값
// 두 번쨰 인수는 각 스트림 요소의 매핑 함수
// 세 번째 인수는 BinaryOperator 함수형 인터페이스
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
// 간소화 코드
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCalories, Integer::sum));
그룹화

특정 데이터를 기준으로 그룹화하는 데 사용
예시 코드
// 간단한 사용 예제
Map<Type, List<Dish>> dishesType =
menu.stream().collect(groupingBy(Dish::getType));
// 좀 더 복잡한 사용 예제
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;
}));
// 요소를 그룰화한 후에는 조작하는 연산이 필요 -> 예) 500칼로리 넘는 요리만 필터링
// 아래 예제를 통해 해결할 수 있을 것처럼 보이지만 Fish Type 메뉴가 필터링 되면 분류도 사라짐
Map<Type, List<Dish>> caloricDishesType = menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getCalories));
// 위 이슈 해결을 위해 groupingBy는 Predicate를 인자로 받음
Map<Type, List<Dish>> caloricDishesByType = menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalries() > 500,
toList())));
// mappping 메서드를 통해 List<String> 으로 변환 가능
Map<Type, List<String>> dishNameByType = menu.stream()
.collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
// flatMapping을 통해 2차원 -> 1차원 변환 가능
Map<Type, Set<String>> dishNamesByType = menu.stream()
.collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));
다수준 그룹화
두 인수를 받는 Collectors.groupingBy() 를 이용하면 항목을 다수준으로 그룹화 가능
Collectors.groupingBy() 는 일반적인 분류 함수와 컬렉터를 인수로 받음
즉, 두 번쨰 인자로 Collectors.groupingBy() 를 전달할 수 있다는 것
예시 코드
// 결과로 두 수준의 맵이 만드는 코드
Map<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;
})
)
);
서브 그룹으로 데이터 수집
groupingBy() 메서드의 두 번쨰 인자로 전달하는 Collectors의 제한은 없음
예를 들면 Collectors.couning()을 전달해서 그룹별 개수로 데이터 수집 가능
한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 함축형
예시 코드
Map<Type, Long> typesCount = menu.stream()
.collect(groupingBy(Dish::getType, counting());
// result -> {MEAT=3, FISH=2, OTHER=4}
// 요리를 Type으로 구분하고 각 Type별 가장 높은 칼로리 메뉴 추출
Map<Dish, Optional<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
컬렉터 결과를 다른 형식에 적용하기
마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있음
Collectors.collectingAndThen() 으로 컬렉터가 반환한 결과를 다른 형식으로 활용 가능
collectingAndThen()은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환
예시 코드
Map<Type, Dish> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType,
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),
Optional::get))):
groupingBy와 함께 사용하는 다른 컬렉터 예제
일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 떄는 팩토리 메서드 groupingBy() 에 두 번쨰 인수로 전달한 컬렉터 활용
예를 들어 메뉴에 있는 모든 요리의 칼로리 합계를 구하려고 만든 컬렉터 재사용 가능
예시 코드
Map<Type, Integer> totalCaloriesByType = menu.stream()
.collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
분할
분할은 분할 함수로 불리는 Predicate를 분류 함수로 사용하는 특수한 그룹화 기능
분할 함수는 boolean을 반환하므로 맵의 키는 boolean으로 이루어짐 → 그룹은 최대 2개만 가능
예시 코드
// 앞선 groupingBy와 가장 큰 차이점은 Predicate를 첫 번쨰 인수로 받는다는 점
Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
.collect(partitioningBy(Dish::isVegetarian));
// Map응 이용해 채식 요리를 얻을 수 있음
List<Dish> vegetarianDishes = partitionedMenu.get(true);
분할의 장점
partitioningBy() 는 두 번쨰 인수로 Collectors를 전달할 수 있는 오버라이딩된 함수도 존재Collectors 클래스의 정적 팩토리 메서드 정리
Collectors 인터페이스
232P 다시 보기