[모던 자바 인 액션] 06. 스트림으로 데이터 수집

seony·2023년 3월 12일
0

모던 자바 인 액션

목록 보기
6/6
post-thumbnail

중간 연산

  • filter, map
  • 한 스트림을 다른 스트림으로 변환하는 연산
  • 스트림의 요소를 소비하지 않는다.

최종 연산

  • count, findFirst, forEach, reduce
  • 스트림의 요소를 소비해서 최종 결과를 도출한다.

6.1 Collector 란 무엇인가?

❗️기억하기
stream.collect() 는 인자로 Collector 인터페이스 인스턴스를 받고,
Collectors의 toList(), counting() 등은 Collector 형을 return한다.

1. 고급 리듀싱 기능을 수행하는 Collector

  • 함수형 API의 장점
    • 높은 수준의 조합성과 재사용성
  • 스트림에 collect를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.
  • collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.
  • Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정된다.
List<Transaction> transactions = transactionStream.collect(Collectors.toList());
  • Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다. toList()

2. 미리 정의된 Collector

  • Collectors에서 제공하는 메서드의 기능을 크게 3가지로 구분할 수 있다.
  1. 스트림 요소를 하나의 값으로 리듀스하고 요약
  2. 요소 그룹화
  3. 요소 분할


6.2 리듀싱과 요약

  • 컬렉터(Stream.collect 메서드의 인수)로 스트림의 항목을 컬렉션으로 재구성할 수 있다.
  • 좀 더 일반적으로 말해 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.

🎈 Collectors.counting()

long howManyDishes = menu.stream().collect(Collectors.counting()); // 방법 1
long howManyDishes2 = menu.stream().count(); // 방법 2
  • counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.

1. 스트림값에서 최댓값과 최솟값 탐색

🎈 Collectors.maxBy(), Collectors.minBy()

  • 두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

Optional<Dish> mostCaloriesDish = menu.stream()
									  .collect(maxBy(dishCaloriesComparator));

2. 요약 연산

🎈 Collectors.summingInt()

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
  • summingInt는 객체를 int로 매핑하는 함수를 인수로 받는다.
public interface ToIntFunction<T> {
    int applyAsInt(T value);
}

Collectors.summingLongCollectors.summingDouble 메서드는 같은 방식으로 동작하며 각각 long 또는 double 형식의 데이터로 요약한다는 점만 다르다.

🎈 Collectors.averagingInt()

  • 평균값 계산 등의 연산도 요약 기능으로 제공한다.
  • Collectors.averagingLong(), Collectors.averagingDouble 등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.

🎈 Collectors.summarizingInt()

  • 하나의 요약 연산으로 요소 수, 합계, 평균, 최댓값, 최솟값 등을 계산해준다.

3. 문자열 연결

🎈 joining()

  • 컬렉터에 joining() 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
  • 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.
  • 구분 문자열을 넣을 수 있도록 오버로드된 joining 팩터리 메서드도 있다.

지금까지 살펴본 모든 컬렉터는 다음에 나오는 reducing 팩토리 메서드로도 정의할 수 있다. 즉, Collectors.reducing으로도 구현할 수 있다.

4. 범용 리듀싱 요약 연산

아래와 같이 reducing 메서드(인자 3개)로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.

<인자 3개인 reducing>

int totalCalories = menu.stream()
						.collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

int totalCalories = menu.stream()
						.collect(reducing(0, Dish::getCalories, (Integer::sum));
  • 첫 번째 인수 0
    • 리듀싱 연산의 시작 값이거나
    • 스트림에 인수가 없을 때는 반환 값
  • 두 번째 인수 Dish::getCalories
    • 요리 객체 -> Integer로 변환할 때 사용한 변환 함수
  • 세 번째 인수 (i, j) -> i + j
    • 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator

<인자 1개인 reducing>

Optional<Dish> mostCaloriesDish = menu.stream()
								      .collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

➜ 값이 없는 빈 스트림이 넘겨졌을 때 시작값이 없어 문제가 생기므로 Optional<Dish> 객체를 반환하는 것

💡 collect와 reduce

  • collect와 reduce로 같은 기능을 구현할 수 있다.
  • 그러나 의미론적인 문제와 실용성 문제 등 두 가지 문제가 발생할 수 있다.
  • collect : 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드
  • reduce : 두 값을 하나로 도출하는 불변형 연산
    ➜ 실용성 문제 발생 : 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 점


6.3 그룹화

  • 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스(groupby)에서 많이 수행되는 작업이다.
Map<Dish.Type, List<Dish>> dishesByType = menu.stream()
											  .collect(groupingBy(Dish::getType));
  • 함수를 기준으로 스트림이 그룹화되므로 이를 분류 함수라고 부른다.
  • 그룹화 함수가 반환하는 그리고 각 키에 대응하는 스트림의 모든 항목 리스트를 으로 갖는 맵이 반환된다.
  • 단순한 속성 접근자(Dish::getType 등) 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.
  • 메서드 참조 대신 람다 표현식으로 필요한 로직 구현 가능

1. 그룹화된 요소 조작

요소를 그룹화 한 다음에는 요소들을 조작하는 연산이 필요하다.

🎈 Collectors.filtering 적용

  • 아래 코드는 FISH 종류 요리가 없을 경우 결과 맵에서 해당 키 자체가 사라지는 문제 해결
Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
			 .collect(groupingBy(Dish::getType, Collectors.filtering(dish -> dish.getCalories() > 500, toList())));

결과

{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}

🎈 Collectors.mapping 적용

Map<Dish.Type, List<String>> dishNamesByType = menu.stream()
			.collect(groupingBy(Dish::getType, Collectors.mapping(Dish::getName, toList())));

결과로 Dish 객체가 아닌 요리 이름이 들어감
Map<Dish.Type, List<Dish>>Map<Dish.Type, List<String>>

2. 다수준 그룹화

칼로리 같은 한 가지 기준으로 메뉴의 요리를 그룹화했는데 두 가지 이상의 기준을 동시에 적용할 수 있을까?
➜ 효과적으로 조합할 수 있다는 것이 그룹화의 장점

  • Collectors.groupbingBy는 일반적인 분류 함수컬렉터를 인수로 받는다.
  • 즉, 바깥쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를 전달해서 두 수준으로 스트림의 항목을 그룹화할 수 있다.
Map<Dish.Type, Map<CaloricLevel, 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;
                        })
                	)
                );
  • 보통 groupingBy 연산을 '버킷' 개념으로 생각하면 쉽다.
    • 첫 번째 groupingBy는 각 키의 버킷을 만든다.
    • 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채워가기를 반복하면서 n수준 그룹화를 달성한다.

3. 서브그룹으로 데이터 수집

  • 다음과 같이 groupingBy 컬렉터에 두 번째 인수로 counting 컬렉터를 전달할 수 있다.
Map<Dish.Type, Long> typesCount = menu.stream().collect(
													groupingBy(Dish::getType, counting()));
{MEAT=3, FISH=2, OTHER=4}

🎈 Collectors.collectingAndThen

Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
					.collect(groupingBy(Dish::getType, // 분류 함수
                                        collectingAndThen(maxBy(comparingInt(Dish::getCalories)), // 감싸진 컬렉터
                                             		      Optional::get))); // 변환 함수
  • collectingAndThen적용할 컬렉터변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
  • 이 예제에서는 maxBy로 만들어진 컬렉터가 감싸지는 컬렉터이며 변환 함수 Optional::get으로 반환된 Optional에 포함된 값을 추출한다.
  • 리듀싱 컬렉터는 절대 Optional.empty()를 반환하지 않으므로 안전한 코드다.

0개의 댓글