이번장에서는 collect를 사용하여 다양한 요소 누적 방식을 알아가 보겠습니다.
6.1 컬렉터란 무엇인가?
- Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
컬렉터의 최대 강점은 결과를 수집하는 과정을 유연하게 정의할 수 있다는 점이다.
스트림에서 collect를 호출하면 collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 수행한다.
6.1.2 미리 정의된 컬렉터
Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분된다.
- 스트림 요소를 하나의 값으로 리듀스 하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
컬렉터로 스트림의 항목을 컬렉션으로 재구성 즉 하나의 결과로 합칠 수 있다.
첫 번째 예제로 counting이라는 메서드를 이용하여 수를 계산해보자.
long howManyDishes = menu.stream().collect(Collectors.counting());
//collect 생략 기능을 이용하여 간단하게 표현
long howManyDishes = menu.stream().count();
6.2.1 스트림 값에서 최댓값과 최솟값 검색
Collectors.maxBy, Collectors.minBy 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.
두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
6.2.2 요약 연산
- Collectors 클래스는 Collectors.summingInt라는 요약 팩토리 메서드를 제공한다.
- summingInt는 객체를 int로 매핑하는 함수를 인수로 받으며, 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.
// 총 칼로리 계산
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
6.2.3 문자열 연결
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
문자열 간 구분 짓고 싶다면 joining 안에 "," 이런 식으로 넣어주면 된다.
6.2.4 범용 리듀싱 요약 연산
리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출한다.
예를 들어 다음 코드처럼 reducing 메서드로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.
int totalCalrories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j);
또는 한 개의 인수를 가진 reducing 버전을 이용해서 가장 칼로리가 높은 요리를 찾을 수도 있다.
Optional<Dish> mostCaloriesDish = menu.stream().collect(reducing(d1, d2)
-> d1.getCalories() > d2.getCalories() ? d1 : d2));
람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum));
counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서 구현할 수 있다.
public static <T> Collector<T, ?, Long> counting() {
return reducing(0L, e -> 1L, Long::sum);
}
자신의 상황에 맞는 최적의 해법 선택
함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있다.
컬렉터를 이용하면 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 코드가 복잡해지지만,
재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 얻을 수 있다.
6.3 그룹화
그룹화란? 데이터 집합을 하나 이상의 특성으로 분류하는 것이다.
명령형으로 그룹화를 구현하려면 할 일과 에러가 많아진다. 하지만 자바 8의 함수형을 이용하면 그룹화를 쉽게 구현할 수 있다.
Collectors.groupingBy를 이용해서 그룹화를 해보자.
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
Map에 포함된 결과는 다음과 같다.
{FISH=[aa,bb], MEAT=[cc,dd]}
스트림의 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupbingBy 메서드로 전달했다.
이 함수를 기준으로 스트림이 그룹화되므로 이를 분류 함수라고 부른다.
더 복잡한 기준으로 분류를 한다고 하면은?
public enum CaloricLevel { DIET, NORMAL, FAT }
// 기준 400 , 700
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 그룹화된 요소 조작
요소를 그룹화 한 다음에 그룹의 요소를 조작하는 연산을 groupingBy메서드를 이용해서 만들어보자.
필터 적용 전에 처리할 수 있지만, 없는 값은 KEY 자체가 사라져 버리는 단점이 있다.
Map<Dish, Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType, filtering(dish -> getCalrories() > 500, toList())));
filtering 메서드는 Collectors 클래스의 또 다른 정적 팩토리 메서드로 프레디케이트를 인수로 받는다. 이 프레디케이트로 각 그룹의 요소와 필터링된 요소를 재그룹화한다.
그룹화된 항목을 조작하는 다른 기능 중 하나는 매핑 함수를 이용해 요소를 변환하는 작업이다.
mapping 메서드를 이용하여 이름 목록으로 변화해보자.
Map<Dish, Type, List<Sting>> dishNamesByTypes = menu.stream()
.collect(groupingBy(Dish::Type, mapping(Dish::getName, toList())));
그룹화는 두 가지 이상의 기준을 동시에 적용할 수 있다는 게 큰 장점이다.
6.3.2 다수준 그룹화
두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화할 수 있다.
Map<Dish.Type, Map<CalricLevel, 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;
})
)
};
외부 맵은 첫 번째 수준의 분류함수에서 분류한 키값 'fish, meat, other'를 가지며, 내부 맵은 두 번째 분류 함수의 키값 'normal, diet, fat'을 가진다.
보통 groupingBy의 연산을 '버킷(물건을 담을 수 있는 양동이)' 개념으로 생각하면 쉽다.
첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채워가기를 반복하면서 n수준 그룹화를 달성한다.
6.3.3 서브그룹으로 데이터 수집
groupingBy 메서드의 두번째 인수로 전달받는 컬렉터의 형식은 제한이 없다.
분류 함수 한개의 인수를 갖는 groupingBy(f)는 groupingBy(f, toList())의 축약형일 뿐이며, 다양한 컬렉터를 전달받을 수 있다.
// counting 컬렉터를 전달해서 메뉴에서 요리의 수를 종류별로 계산할 수 있다.
Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
// 가장 높은 칼로리를 가진 요리 찾기
Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType, maxBy(CompaingInt(Dish::getCalories))));
'자바 > 모던 자바 인 액션' 카테고리의 다른 글
[모던 자바 인 액션] 7장. 병렬 데이터 처리와 성능 (0) | 2022.10.24 |
---|---|
[모던 자바 인 액션] 6장. 스트림으로 데이터 수집(2) (0) | 2022.10.24 |
[모던 자바 인 액션] 5장. 스트림 활용 (0) | 2022.09.29 |
[모던 자바 인 액션] 4장. 스트림이란 (0) | 2022.09.15 |
[모던 자바 인 액션] 3장. 람다 표현식 (0) | 2022.09.08 |