티스토리 뷰
컬렉터란 무엇인가?
고급 리듀싱 기능을 수행하는 컬렉터
훌륭하게 설계된 함수형 API의 또 다른 장점으로 높은 수준의 조합성과 재사용성을 꼽을 수 있다. collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 최대 강점이다. collect 에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.
Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.
미리 정의된 컬렉터
Collectors에서 제공하는 메서드의 기능은 크게 세가지로 구분
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
리듀싱과 요약
컬렉터 (Stream.collect 메서드의 인수)로 스트림의 항목을 컬렉션으로 재구성 할 수 있다. 좀 더 일반적으로 말해 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.
트리를 구성하는 다수준 맵, 메뉴의 칼로리 합계를 가리키는 단순한 정수 등 다양한 형식으로 결과가 도출될 수 있다.
스트림값에서 최댓값과 최솟값 검색
Collectors.maxBy, Collectors.minBy 두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
요약 연산
스트림에 있는 객체의 숫자 필드의 ㅎ바계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용된다. 이러한 연산을 요약 연산이라 부른다.
summintInt는 객체를 int로 매핑하는 함수를 인수로 받는다. summingInt의 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다.
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
//reducing 연산
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get()
단순 합계 외에도 평균값 계산 등의 연산도 요약 기능으로 제공
- averagingInt - 평균값 계산
- summarizingInt - 요소 수, 합계, 평균, 최댓값, 최솟값을 계산하는 코드
private static IntSummaryStatistics calculateMenuStatistics() {
return menu.stream().collect(summarizingInt(Dish::getCalories));
}
//IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
문자열 연결
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다. 연결된 두 요소 사이에 구분 문자열을 넣을 수도 있다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
지금까지 모든 컬렉터는 Collectors.reducing 팩토리 메서드로도 정의할 수 있다. 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문인데 둘 중에 가독성이 좋은 것으로 프로그래밍 하는 것이 중요하다.
그룹화
자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.
Collectors.groupingBy를 이용해서 쉽게 메뉴를 그룹화
private static Map<Dish.Type, List<Dish>> groupDishesByType() {
return menu.stream().collect(groupingBy(Dish::getType));
}
//{MEAT=[pork, beef, chicken], OTHER=[french fries, rice, season fruit, pizza], FISH=[prawns, salmon]}
스트림의 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupingBy 메서드로 전달. 이 함수를 기준으로 스트림이 그룹화 되므로 이를 분류함수라고 부른다.
그룹화된 요소 조작
요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산이 필요. Collectors 클래스는 일반적인 분류 함수에 Collector 형식의 두 번째 인수를 갖도록 groupingBy 팩토리 메서드를 오버로드해 이 문제를 해결한다.
private static Map<Dish.Type, List<Dish>> groupCaloricDishesByType() {
return menu.stream().collect(
groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));
}
filtering 메소드는 Collectors 클래스의 또 다른 정적 팩토리 메서드로 프레디케이트를 인수로 받는다. 이 프레디케이트로 각 그룹의 요소와 필터링 된 요소를 재그룹화 환다.
또한 Collectors 클래스는 매핑 함수와 각 항목에 적용한 함수를 모으는 데 사용하는 또 다른 컬렉터를 인수로 받는 mapping 메서드를 제공한다.
private static Map<Dish.Type, List<String>> groupDishNamesByType() {
return menu.stream().collect(
groupingBy(Dish::getType,
mapping(Dish::getName, toList())));
}
다수준 그룹화
Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받는다. 즉, 바 깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목을 그룹화할 수 있다.
private static Map<Dish.Type, Map<CaloricLevel, List<Dish>>> groupDishedByTypeAndCaloricLevel() {
return 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;
}
})
)
);
}
//{MEAT={DIET=[chicken], FAT=[pork], NORMAL=[beef]}, OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}, FISH={DIET=[prawns], NORMAL=[salmon]}}
다수준 그룹화 연산은 다양한 수준으로 확장할 수 있다. 즉, n수준 그룹화의 결과는 n수준 트리 구조로 표현되는 n수준 맵이 된다.
서브그룹으로 데이터 수집
groupingBy 컬렉터에 두 번째 인수로 counting 컬렉터를 전달해서 메뉴에서 요리의 수를 종류별로 계산할 수 있다.
private static Map<Dish.Type, Long> countDishesInGroups() {
return menu.stream().collect(groupingBy(Dish::getType, counting()));
}
//{MEAT=3, OTHER=4, FISH=2}
컬렉터 결과를 다른 형식에 적용하기
private static Map<Dish.Type, Dish> mostCaloricDishesByTypeWithoutOprionals() {
return menu.stream().collect(
groupingBy(Dish::getType, //분류 함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), //감싸인 컬렉터
Optional::get))); //변환 함수
}
//{MEAT=pork, OTHER=pizza, FISH=salmon}
컬렉터를 중첩시 가장 외부 계층에서 안쪽으로 다음과 같은 작업이 수행된다.
- 가장 바깥쪽에 위치하면서 요리의 종류(Dish.Type)에 따라 서브스트림으로 그룹화 한다
- groupingBy 컬렉터는 collectingAndThen으로 컬렉터를 감싼다. 따라서 두 번째 컬렉터는 그룹화된 서브스트림에 적용된다.
- collectingAndThen 컬렉터는 세번째 컬렉터인 maxBy를 감싼다.
- 리듀싱 컬렉터(maxBy)가 서브스트림에 연산을 수행한 결과에 Opional::get 변환 함수가 적용된다.
- groupingBy 컬렉터가 반환하는 맵의 분류 키에 대응하는 값이 각각의 Dish에서 가장 높은 칼로리이다.
분할
분할은 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean 이다.
private static Map<Boolean, List<Dish>> partitionByVegeterian() {
//채식주의자 친구를 위한 모든 요리를 채식 요리와 아닌 요리로 분류
//분할함수 partitioningBy
return menu.stream()
.collect(partitioningBy(Dish::isVegetarian));
}
//{false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
분할의 장점
분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점이다.
또한 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드 된 버전의 partitioningBy 메서드도 있다.
private static Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType() {
return menu.stream()
.collect(partitioningBy(Dish::isVegetarian, //분할 함수
groupingBy(Dish::getType))); //두 번째 컬렉터
}
//{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fries, rice, season fruit, pizza]}}
- Total
- Today
- Yesterday
- maven
- Linux
- 베리 심플
- intellij
- JavaScript
- oracle
- Github Status
- config-location
- svn
- Spring Security
- mybatis
- mybatis config
- elasticsearch
- Spring
- Java
- input
- window
- jQuery
- docker
- k8s
- 북리뷰
- springboot
- rocky
- 오라클
- Mac
- LocalDate
- Bash tab
- localtime
- LocalDateTime
- Kotlin
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |