Search
Duplicate
📒

[Java Study] 11-x. 스트림으로 데이터 수집

상태
미진행
수업
Java Study
주제
Stream
4 more properties
참고

컬렉터(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까지의 숫자중 소수인값과 아닌값을 분류한다.