어떤 트랜잭션 리스트가 있는데 이들을 액면 통화(달러, 원화 등등)로 그룹화 한다고 가정하자.
람다가 없는 시절에는 이렇게 작성해야 했다.
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<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
위 코드는 익숙하지만 너무 길다. 스트림에 컬렉터 파라미터를 collect 메서드에 전달함으로써 원하는 연산을 간결하게 구할 수 있다.
Map<Currency, List<Transaction>> transactionsByCurrenciesByCurrency =
transactions.stream().collect(groupingBy(Transaction::getCurrency));
6.1 컬렉터란 무엇인가?
- 함수형 프로그래밍에서는 '무엇'을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경쓸 필요가 없다.
- 다수준으로 그룹화 할 때 명령형 프로그래밍과 함수형 프로그래밍의 차이점이 두드러진다.
- 명령형 코드는 다중 루프와 조건문을 추가하여 가독성과 유지보수성이 크게 떨어진다.
- 반면, 함수형 프로그래밍에서는 필요한 컬렉터들을 쉽게 추가할 수 있다.
6.1.1) 고급 리듀싱 기능을 수행하는 컬렉터
- 함수형 api의 또 다른 장점은 높은 수준의 조합성과 재사용성이다.
- collect 에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하며 컬렉터가 작업을 처리한다.
- 보통 함수를 요소로 변환할 때는 컬렉터를 적용하여 최종 결과를 저장하는 자료구조에 값을 누적한다.
6.1.2) 미리 정의된 컬렉터
6장에서는 Collectors 클래스에서 제공하는 팩터리 메서드의 기능을 설명한다.
- 스트림 요소를 하나의 값으로 리듀스 하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
- 컬렉터로 스트림의 항목을 컬렉션으로 재구성할 수 있다.
long howManyDishes = menu.stream().collect(Collectors.counting());
6.2.1) 스트림에서 최댓값, 최솟값 검색
- Collectors.maxBy, Collectors.minBy 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator 를 인수로 받는다.
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(Comparator.comparingInt(Dish::getCalories)));
6.2.2) 요약 연산
- 합계나 평균 등을 반환한다.
- Collectors.summingInt(Long, double) : 객체를 int로 매핑하는 함수를 인수로 받는다.
- Collectors.averagingInt(Long, double) : 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.
- 합계, 평균, 최댓값, 최솟값을 계산해주는 summarizingInt 도 있다 !
6.2.3) 문자열 연결
- 컬렉터에 joining 팩토리 메서드를 이용하면 문자열을 하나의 문자열로 연결할 수 있다. (내부적으로 StringBuilder를 사용한다.)
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
// 구분자도 넣을 수 있다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
6.2.4) 범용 리듀싱 요약 연산
- 지금까지 살펴본 모든 컬렉터는 reducing 메서드로도 정의할 수 있다.
- 그럼에도 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다.
- 컬렉션 프레임워크의 유연성 : 같은 연산도 다양한 방식으로 수행이 가능하다.
6.3 그룹화
- 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다.
```java
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
- getType과 같이 기준이 되는 함수를 분류함수라고 한다.
- 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조 대신 람다를 이용할 수 있다.
지금까지는 한 가지를 기준으로 그룹화하는 것을 알아봤다. 이제는 두 가지 기준으로 동시에 그룹화하는 것을 알아보자.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; }));
6.3.1) 그룹화된 요소 조작
- 요소를 그룹화한 다음에는 각 결과 그룹의 요소를 조작하는 연산이 필요하다.
- [예제] 500 칼로리 이상의 요리만 그룹화한다.
Map<Dish.Type, List<Dish>> caloricDishesbyType = menu.stream().filter(dish -> dish.getcalories() >= 500) .collect(groupingBy(Dish::getType));
{OTHER = [french fires, pizza], MEAT = [pork, beef]}
다음과 같은 결과값이 나온다. 문제는 FISH라는 키값도 있었는데 아예 사라져버렸다는 것 !
다음은 수정된 코드이다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));
위 코드처럼 두 번째 Collector 안으로 필터 프레디케이트를 넣어줌으로 문제를 해결할 수 있다.
filtering 메서드는 Collectors 클래스의 또 다른 정적 팩토리 메서드로 프레디케이트를 인수로 받는다.
이 프레디케이트로 각 그룹의 요소와 필터링 된 요소를 재그룹화 한다.
{OTHER = [french fires, pizza], MEAT = [pork, beef], FISH=[]}
- 그룹화된 항목을 조작하는 다른 유용한 기능 중 또 다른 하나로 mapping 메서드가 있다.
Map<Dish.Type, List<String>> dishNamesByType = menu.stream() .collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
6.3.2) 다수준 그룹화
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;
})
)
);
6.3.3) 서브 그룹으로 데이터 수집
- groupingBy 메서드의 두번째 인수로 전달받는 컬렉터의 형식은 제한이 없다.
-
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting())); //{MEAT=3, FISH=2, OTHER=4} Map<Dish.Type, Optional> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, maxBy(CompaingInt(Dish::getCalories)))); //{FISH=Optional[salmon], OTHER=Optional[pizza], MEAT=Optional[pork]}
- 분류 함수 한개의 인수를 갖는 groupingBy(f)는 groupingBy(f, toList())의 축약형일 뿐이며, 다양한 컬렉터를 받을 수 있다.
컬렉터 결과를 다른 형식에 적용하기
- Collectors.collectingAndThen 팩토리 메서드로 컬렉터가 반환한 결과를 다른 형식으로 활용할 수 있다.
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}
groupingBy와 함꼐 사용하는 다른 컬렉터 예제
일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때에는 팩토리 메서드 groupingBy에 두 번째 인수로 전달한 컬렉터를 사용한다.
Map<Dish.Type, Integer> totalCaloriesByType = menu.stream()
.collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
이 외에도 mapping 메서드로 만들어진 컬렉터도 groupingBy와 자주 사용된다.
mapping은 입력 요소를 누적하기 전에 매핑 함수를 적용해서 다양한 형식의 객체를 주어진 형식의 컬렉터에 맞게 변환한다.
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() )));
//{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}
'Book > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] Chapter 07. 병렬 데이터 처리와 성능 (1) (0) | 2024.10.29 |
---|---|
[모던 자바 인 액션] Chapter 06. 스트림으로 데이터 수집 (2) (1) | 2024.10.25 |
[모던 자바 인 액션] Chapter 05. 스트림 활용 (0) | 2024.10.01 |
[모던 자바 인 액션] Chapter 04. 스트림 소개 (1) | 2024.10.01 |
[모던 자바 인 액션] Chapter 03. 람다 표현식 (1) | 2024.09.25 |