이 장의 내용
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를 전달할 수 있는 오버라이딩된 함수도 존재
예시 코드
Map<Boolean, Map<Type, List<Dish>>> vegetarianDishesByType = menu.stream()
.collect(partitioningBy(Dish::getType,
groupingBy(Dish::getType)));
// 위를 더 활용하여 채식/비채식 요리에서 가장 높은 칼로리 요리도 찾을 수 있음
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian = menu.stream()
.collect(partitioningBy(Dish::isVegetarian,
collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
Optional::get)));
Collectors 클래스의 정적 팩토리 메서드 정리
Collectors 인터페이스
리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합으로 구성
커스텀으로 Collector 인터페이스를 구현하는 리듀싱 연산도 만들 수 있음
Collector 인터페이스
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 -> 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대개 컬렉션 형식)
// Stream<T>의 모든 요소를 List<T>로 수집하는 ToListCollector<T> 클래스 생성 가능
public class ToListCollector<T> implmenets Collector<T, List<T>, List<T>>
supplier 메서드(새로운 결과 컨테이너 만들기)
supplier()
메서드는 빈 결과로 이루어진 Supplier를 반환해야 함
즉, supplier()
는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
앞선 ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정이 결과가 될 수 있음
예시 코드
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
// 생성자 참조를 전달하는 방법
public Supplier<List<T>> supplier() {
return ArraysList::new;
}
accumulator 메서드(결과 컨테이너에 요소 추가하기)
accumulator()
메서드는 리듀싱 연산을 수행하는 함수를 반환
스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용
함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부상태가 바뀌므로 누적자가 어떤 값일지 단정 불가능
ToListCollector에서 accumulator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산 수행
예시 코드
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
// 코드 간결화
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
finisher 메서드(최종 변환값을 결과 컨테이너로 적용)
finisher()
메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환
때로는 ToListCollector에서 볼 수 있는 것처럼 누적자 객체가 이미 최종 결과인 상황도 있음 → 이 상황에서는 변환 과정이 필요 없으므로 finisher 메서드는 항등 함수 반환
예시 코드
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
combiner 메서드(두 결과 컨테이너 병합)
characteristic 메서드
232P 다시 보기