스트림으로 데이터 수집
- Collectors 클래스로 컬렉션 만들고 사용하기
- 하나의 값으로 데이터 스트림 리듀스 하기
- 특별한 리듀싱 요약 연산
- 데이터 그룹화와 분할
- 자신만의 커스텀 컬렉터 개발
컬렉션(Collection), 컬렉터(Collector), collect는 서로 다르다.
6.1 컬렉터란 무엇인가?
Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다. 즉, 내부적으로
리듀싱 연산
이 일어난다.
장점 : collect 로 결과를 수집하는과정을 간단하면서도 유연한 방식으로 정의할 수 있다.
예제 6.1
그림 6.1
6.1.2 미리정의된 컬렉터
Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다.
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
counting 예시
int howManyDishes = menu.stream().collect(counting());
6.2.1 스트림값에서 최댓값과 최솟값 검색
Collectors.maxBy 와 Collectors.minBy 로 스트림의 최대값과 최소값을 구할 수 있다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
6.2.2 요약연산
Collectors.summinngInt
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories))
그림 6.2 summinngInt 컬렉터 누적과정
그 외 long, double 과 averagingInt 및 summarizingInt 가 있다.
6.2.3 문자열 연결
joining
추출한 모든 문자열을 하나의 문자열로 연결해서 반환
String menuListStr = menu.stream().map(Dish::getName).collect(joining(", "))
6.2.3 범용 리듀싱 요약 연산
reducing
범용 Collectors.reducing을 사용
특화된 summinngInt 등과 같은 메소드를 사용하는 이유는 편의성 및 가독성 때문
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCaloreis, (i, j) -> i + j));
reducing은 세 개의 인수를 받는다. (초기값, 합계 함수, 변환 함수)
- 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.(숫자 합계에서는 인수가 없을 때 반환하므로 0이 적합하다.)
- 두 번째 인수는 함수를 받는다.
- 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator이다.
한 개의 인수를 갖는 reducing
가장 칼로리가 높은 요리 찾는 방법
Optional<Dish> mostCaloireDish = menu.stream().collect(
reducing((d1, d2) -> d1.getCaloreis() > d2.getCalories() ? d1 : d2)
);
세 개의 인수를 갖는 reducing 메서드에서 첫 번째 인수를 받고, 두 번째 인수에서자기 자신을 그대로 반환하는 항등함수(identity function)
를 두 번째 인수로 받는 상황에 해당한다.
한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌으래 시작값이 설정되지 않아 null을 반환할 수 있으므로
Optional 객체로 만들어 사용해야 한다.
컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
int totalCaloires = menu.stream().collect(reducing(0, Dish::getCaloires, Integer::sum));
menu.stream().map(Dish::getCalories).reduce(Integer::sum).get(); // 인자가 하나여서 Optional 을 반환하지만 get 으로 값 추출
그룹화
자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.
Collectors.groupingBy
/**
* 그룹화 groupingBy
* 생선, 고기 그 밖의 것들로 그룹화
*/
Map<Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
groupingBy를 분류 함수(classification function)
이라고 한다.
그림 6.4 - 그룹화로 스트림의 항목을 분류하는 과정
람다 표현식으로도 그룹화 가능
/**
* 칼로리별로 그룹화
*/
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칼로리가 넘는 요리만 타입과 종류로 그룹화
Map<Type, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalroies() > 500)
.collect(groupingBy(Dish::getType));
/**
* 결과
* {OTHER=[french fries, pizza], MEAT=[pork, beef]}
* FISH 라는 키 자체가 사라짐
* 위 코드의 단점은 위 filter 프레디케이트를 만족하는 값이 없을 경우 키값 자체가 제외되서 맵에 담지 못한다.
* 해결책 : Collectors 클래스의 정적 팩터리 메서드인 filtering 사용
*/
// 해결
Map<Type, List<Dish>> caloricDishesByType = menu.stream().collect(groupingBy
(Dish::getType, filtering(dish -> dish.getCalories() > 500, toList())));
// 결과 : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
/** mapping 사용 */
Map<Type, List<String>> dishNamesByType = menu.stream().
collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
/** flatMapping 사용
* flatMap은 두 수준의 리스트를 한 수준으로 평면화 할 수 있음
* 연산결과를 수집해서 리스트가 아니라 집합으로 그룹화해 중복 태그를 제거한다.
*/
Map<Type, Set<String>> dishNamesByType =
menu.stream()
.collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));
6.3.2 다수준 그룹화
menu.stream().collect(
groupingBy(Dish::getType,
groupingBy((Dish dish) -> {
if (dish.getCalories() <= 400) {
return CaloricLevel.DIET;
}
else if (dish.getCalories() <= 700) {
return CaloricLevel.NORMAL;
}
else {
return CaloricLevel.FAT;
}
})
)
groupingBy
groupingBy(x)는 사실 groupingBy(x, toList())의 축약형이다.
- 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 프로그램
Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCaloires))));
6.3.3 서브그룹으로 데이터 수집
Map<Dish.TYPE, Long> typeCount = menu.stream().collect(groupingBy(Dish::getType, counting()))
컬렉터 결과를 다른 형식에 적용하기
collectingAndThen
팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
분류함수로 여러 서브스트림을 만든 뒤 각각 리듀싱 요약 가능
Map<Type, Dish> mostCaloricByType = menu.stream() .collect(groupingBy(Dish::getType, // 분류함수 collectingAndThen(maxBy(comparingInt(Dish::getCaloreis)), // 감싸인 컬렉터 Optional::get)); // 변환함수 -- Optional 에 포함된 값을 추출
그림 6.6 여러 컬렉터를 중첩한 효과
- 각 요리 형식에 존재하는 모든 CaloricLevel 값을 알고 싶은 경우(groupingBy와 mapping 사용)
Map<Type, Set> caloricLevelByType =
menu.stream().collect(
groupingBy(Dish::getType,
mapping(dish -> { // dish를 CaloricLevel 로 매핑
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700 return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;
},
toSet() )
)
); // 리스트가 아닌 집합으로 반환
6.4 분할(partitioningBy)
분할은 분할 함수(partitioning function)
이라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean
이다.
분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다.
모든 요리를 채식과 아닌 요리로 분류
Map<Boolean, List> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian)); List vegetarianDishes = partitionedMenu.get(true); // 파티셔닝 사용 List vegetarianDishes2 = menu.stream().filter(Dish::isVegetarian).collct(toList()); // 필터사용
분할함수 안에 그룹함수를 사용
menu.stream().collect(partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType)))
채식과 채식이 아닌 요리에서 가장 칼로리가 높은 음식 찾기
분할함수 안에서서 값 비교
Map<Boolean, Dish> mostCaloricPartitioneByVegetarian = menu.stream().collect( partitioningBy(Dish::isVegetarain, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));
6.4.2 숫자를 소수와 비소수로 분할하기
6.5 Collector 인터페이스
public interface Collector<T, A, R> {
Supplier supplier();
BiConsumer<A, T> accumulator();
BinaryOperator combiner();
Function<A, R> finisher();
Set<Collector.Characteristics> characteristics();
}
- T는 수집될 스트림 항목의 제네릭 형식
- A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식
- R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대개 컬렌션 형식)
characteristics 은 collect 메소드가 어떤 최적화를 이용해서 리듀싱 연산을 수행할 것인지를 결정하도록 돕는 힌트 특성 집합 제공
supplier 메소드: 새로운 결과 컨테이너 만들기
supplier 메소드는 빈 결과로 이루어진 Supplier를 반환해야 한다.
accumulator 메소드: 결과 컨테이너에 요소 추가하기
accumulator 메소드는 리듀싱 연산을 수행하는 함수를 반환
finisher 메소드: 최종 변환값을 결과 컨테이너로 적용하기
finisher 메소드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.
combiner 메소드: 두 결과 컨테이너 병합
combiner 는 스트림의 서로 다른 서브파트를 병렬로 처리할 떄 누적자가 이 결과를 어떻게 처리 할지 정의한다.
Characteristics 메소드
Characteristics 는 스트림을 병렬로 리듀스할지 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트 제공
UNORDERED : 리듀싱 결과가 누적순서에 영향 받지 않을때
CONCURRENT : 병렬 리듀싱을 수행할 수 있다. 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱 가능(순서 무의미)
IDENTIFY_FINISH : 리듀싱 과정의 최정결과 - 생략가능
menu.stream().collect(Collectors.toList());
\-> menu.stream().collect(new ToListCollector());
기존 코드의 toList 는 팩토리지만 ToListCollector 는 new 로 인스턴스화한다.
'책정리 > Modern Java In Action' 카테고리의 다른 글
[책 정리]Modern Java In Action 2장 (0) | 2020.05.18 |
---|---|
[책 정리]Modern Java In Action 1장 (0) | 2020.05.18 |
[책 정리]Modern Java In Action 시작 (0) | 2020.05.18 |
[책 정리]Modern Java In Action 5장 (0) | 2020.05.18 |
[책 정리]Modern Java In Action 4장 (0) | 2020.05.18 |