참고
컬렉터(Collector)란 무엇인가?
NOTE
Coolector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할 지 지정한다!
// 통화별로 트랜잭션을 그룹화한 코드
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream().collect(Collectors.groupingBy(Transaction::getCurrency));
Java
복사
Collectors.groupingBy를 이용해서 각 키(통화)에 대한 Map을 만든다!
Collector의 많은 함수
•
Collect
◦
Collector를 매개변수로 하는 스트림의 최종 연산
•
Collector
◦
Collect에서 필요한 메서드를 정의해놓은 인터페이스
•
Collectors
◦
다양한 기능의 Collector를 구현한 클래스
Collectors.메서드 (미리 정의된 컬렉터)
NOTE
Collector에서 제공하는 메서드는 크게 3가지가 존재한다!
리듀싱과 요약
NOTE
Collector로 Stream의 항목을 Collection으로 재구성할 수 있다!
Collector에서 구현하는 방식에 따라 리듀싱의 과정이 달라진다.
스트림 최대값, 최소값 검색 ( maxBy(), minBy() )
NOTE
// Comparator 생성
Comparator<Dish> dishCaloriesComparator =
Comparator.comparingInt(Dish::getCalories);
// maxBy
Optional<Dish> mostCaloriesDish =
menu.stream()
.collect(maxBy(dishCaloriesComparator)); // max(dishCaloriesComparator)
// minBy
Optional<Dish> leastCaloriesDish =
menu.stream()
.collect(minBy(dishCaloriesComparator)); // min(dishCaloriesComparator)
Java
복사
Collectors의 maxBy, minBy 사용
요약 연산 ( summingXXX, averagingXXX, summarizingXXX )
NOTE
// summingXXX
Integer collect1 = Dish.menu.stream()
.collect(Collectors.summingInt(Dish::getCalories));
// averagingXXX
Double collect2 = Dish.menu.stream()
.collect(Collectors.averagingInt(Dish::getCalories));
// summarizingXXX
IntSummaryStatistics collect3 = Dish.menu.stream()
.collect(Collectors.summarizingInt(Dish::getCalories));
Java
복사
Dish 칼로리의 합, 평균, 요약(합, 평균, 최대, 최소 모두 포함된 객쳬)로 반환한다.
IntSummaryStatistics{count=9, sum=4300, min=120, average=477.777778, max=800}
문자열 연결 ( joining() )
NOTE
String shortMenu = Dish.menu.stream()
.map(Dish::getName)
.collect(Collectors.joining(", "));
Java
복사
추출한 문자열을 모두 하나로 연결한다!
pork, beef, chicken, french fries, rice, season fruit, …
범용 리듀싱 요약 연산 ( reducing() )
NOTE
// 최대 칼로리값 반환
Dish.menu.stream()
.collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
// 칼로리값의 합 반환
Dish.menu.stream()
.collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j);
Java
복사
Collector.reducing을 활용한 리듀싱 연산
Collect vs reduce
// collect로 만든 리스트
List<Integer> collectedList = stream.collect(toList());
// reduce로 만든 리스트
List<Integer> reducedList = stream.reduce(
new ArrayList<>(),
(List<Integer> l, Integer e) -> { // 누적자
l.add(e);
return l;},
(List<Integer> l1, List<Integer> l2) -> { // 결합자
l1.addAll(l2);
return l1;});
Java
복사
2개의 메서드는 다르지만, 같은 기능을 구현할 수 있다.
•
collect
◦
도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드
◦
컨테이너 관련 작업이면서 병렬성을 확보하려면 collect가 더 바람직하다!
•
reduce
◦
두 값을 하나로 도출하려는 불변형 연산
◦
위 예제에서 reduce는 누적자로 사용된 리스트를 반환하므로 잘못 활용한 경우
◦
여러 스레드가 동시에 같은 데이터 구조체를 고치면, 리스트 자체가 망가진다!
그룹화
NOTE
그룹화 ⇒ 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화 하는 연산이다!
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
Java
복사
Type으로 그룹화해서 Map을 반환한다.
// 400칼로리 이하를 diet, 400~700칼로리를 normal, 700칼로리 초과를 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;
}));
Java
복사
필요한 분류함수를 직접 구현할 수 있다.
•
분류 함수
◦
Collectors.groupingBy()같이 함수를 기준으로 스트림이 그룹화되는 함수
그룹화된 요소 조작 ( filtering(), mapping(), flatMapping() )
NOTE
Map<Dish.Type, List<Dish>> calDish = Dish.menu.stream()
.collect(groupingBy(Dish::getType, Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())));
Java
복사
filtering의 2번째 인수는 필터링된 요소를 재그룹화 한다. (비어있는 목록의 경우도 리스트를 만들어준다)
{MEAT=[pork, beef], FISH=[], OTHER=[french fries, pizza]}
Map<Dish.Type, List<String>> nameDish = Dish.menu.stream()
.collect(groupingBy(Dish::getType, Collectors.mapping(Dish::getName, Collectors.toList())));
Java
복사
mapping의 2번째 인수도 비어있는 경우 빈 리스트를 반환해준다.
//
Map<Dish.Type, Set<String>> dishNamesByType =
Dish.menu.stream()
.collect(groupingBy(Dish::getType, Collectors.flatMapping(dish -> dishTags.get(dish.getName()).stream(), Collectors.toSet())));
Java
복사
두 수준의 리스트를 하나의 수준으로 평면화하기 위해 flatMapping을 사용
{MEAT=[salty, greasy, roasted, fried, crisp], FISH=[roasted, tasty, fresh, delicious], OTHER=[salty, greasy, natural, light, tasty, fresh, fried]}
다수준 그룹화
NOTE
// 타입으로 그룹화 한 다음, 칼로리 크기별로 나눈다.
Map<Dish.Type, Map<CaloricLevel, List<Dish>>> twoGrouping = Dish.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;
}))
);
Java
복사
grouping을 여러번 적용할 수 있음!
{MEAT={NORMAL=[beef], FAT=[pork], DIET=[chicken]}, FISH={NORMAL=[salmon], DIET=[prawns]}, OTHER={NORMAL=[french fries, pizza], DIET=[rice, season fruit]}}
서브그룹으로 데이터 수집( collectionAndThen )
NOTE
// 각 타입별로 칼로리가 가장 높은 음식
Map<Dish.Type, Dish> mostCaloricByType = Dish.menu.stream().collect(
groupingBy(
Dish::getType,
collectingAndThen(
maxBy(Comparator.comparingInt(Dish::getCalories)),
Optional::get)));
Java
복사
{MEAT=pork, FISH=salmon, OTHER=pizza}
컬렉터 중
•
collectingAndThen
◦
적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
◦
반환되는 컬렉터는 기존 컬렉터의 래퍼 역할을 하여 collect 마지막 과정에서 변환함수로 자신이 변환하는 값을 맵핑한다.
분할 (Collectors.partitionBy)
NOTE
분할 ⇒ 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능
// 채식주의자를 위한 음식인가 아닌가?
Map<Boolean, List<Dish>> partitionMenu = Dish.menu.stream()
.collect(Collectors.partitioningBy(Dish::isVegetarian));
// filter로도 동일하게 뽑을 수 있음
List<Dish> vegetarianDishes = Dish.menu.stream()
.filter(Dish::isVegetarian).collect(toList());
Java
복사
{false=[pork, beef, chicken, prawns, salmon],
true=[french fries, rice, season fruit, pizza]}
•
분할 함수는 boolean을 반환하므로 Map의 키형식은 Boolean값이다.
•
그룹화 맵은 최대 2개의 그룹으로 분류된다.
분할의 장점
NOTE
분할 함수가 반환하는 참, 거짓 2가지 요소의 스트림 리스트를 모두 유지한다는 점!
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishByType = Dish.menu.stream().collect(
Collectors.partitioningBy(Dish::isVegetarian, groupingBy(Dish::getType))
);
Java
복사
{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]},
true={OTHER=[french fries, rice, season fruit, pizza]}}
•
컬렉터를 2번째 인수로 전달할 수 있는 오버로드된 버전의 partitioningBy 메서드도 있다
숫자를 소수와 비소수로 분할하기
NOTE
정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누는 프로그램 구현을 해보자
public static boolean isPrime(int candidate) {
int candidateRoot = ((int) Math.sqrt((double) candidate));
return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}
Java
복사
이 수가 소수인지 판별한다. ( 2~n까지 나누었을때 나머지가 0인게 없어야 한다. )
public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2, n)
.boxed()
.collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
}
System.out.println(partitionPrimes(10));
// {false=[4, 6, 8, 9, 10], true=[2, 3, 5, 7]}
Java
복사
1~10까지의 숫자중 소수인값과 아닌값을 분류한다.