Collector 개념

지니🧸·2023년 4월 11일
0

Java

목록 보기
5/13

🎞️ 컬렉터란

collect 메서드에 Collector 인터페이스 구현을 전달한다

🎞️ 고급 리듀싱 기능 수행

스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행됨

  • collect에서 리듀싱 연산을 이용해 스트림의 각 요소를 방문하면서 Collector가 작업 처리
  • 보통 함수를 요소로 변환할 때는 컬렉터를 적용 > 최종 결과를 저장하는 자료구조에 값 누적

🎞️ 미리 정의된 Collector

Collector에서 제공하는 메서드의 기능

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

🎞️ 스트림 최댓값/최솟값

요약연산: 스트림에 있는 객체의 숫자 필드의 합계/평균 등 반환하는 연산

  • 여기에도 리듀싱 기능이 사용됨
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

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

🎞️ 요약 연산

Collectors.summingInt (Collectors.summingLong, Collectors.summingDouble)

  • 객체를 int로 매핑하는 함수를 인수로 받음
  • 인수로 받은 객체를 int로 매핑한 컬렉터 반환
  • summingIntcollect 메서드로 전달되면 요약 작업 수행
int totalCalories = menu.stream()
					.collect(summingInt(Dish::getCalories));
  • 평균값 계산: averagingInt, averagingLong, averagingDouble
  • 값 수, 합계, 평균, 최댓값, 최솟값 등 한번에 계산:
    • summarizingInt, summarizingLong, summarizingDouble

🎞️ 문자열 연결

컬렉터에 joining 팩토리 메서드 사용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환함

  • joining 메서드는 내부적으로 StringBuilder를 사용해서 문자열을 하나로 만듬
  • joining의 인수로 문자열을 넘기면 각 객체의 toString 결과값 사이에 문자열 끼워넣기 가능
String shortMenu = menu.stream().map(Dish::getName).collect(joining(" "));

🎞️ 범용 리듀싱 요약 연산

지금까지 쓰인 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있음

  • Collectors.reducing으로 구현 가능
int totalCalores = menu.stream()
					.collect(
                    	reducing(0, Dish::getCalories, (i, j) -> i + j)
                    );
                        

reducing의 인자 3개
1. 첫번째 인수: 리듀싱 연산의 시작값. 스트림에 인수가 없을 때는 반환값
2. 두번째 인수: 요리 -> 칼로리값 변환 함수
3. 세번째 인수: 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator

🎞️ 같은 연산도 다양한 방식으로

int totalCalories = menu.stream()
					.collect(
                    	reducing(0, Dish::getCalories, Integer::sum)
                    );                        

위 코드와 아래 코드는 같은 값을 반환함

int totalCalories = menu.stream()
					.map(Dish::getCalories)
                    .reduce(Integer::sum)
                    .get()

아래 코드도 같은 값을 반환함

int totalCalories = menu.stream()
					.mapToInt(Dish::getCalories)
                    .sum();

🎞️ 그룹화

분류함수(groupingBy): 스트림이 그룹화되는 기준이 되는 함수

  • 결과: 그룹화 함수가 반환하는 키와 각 키에 대응하는 스트림의 모든 항목 리스트를 값으로 갖는 맵
Map<Dish.Type, List<Dish>> dishesByType 
			= menu.stream()
				.collect(groupingBy(Dish::getType));

🎞️ 그룹화된 요소 조작

filtercollect를 같이 사용해 필터링 조건과 그룹화를 함께 수행할 때, 두 함수를 순차적으로 사용하면 필터링해서 모든 값이 걸러진 키는 결과에 존재도 안하게 된다. 주로 프로그래머가 원하는 것은 키는 존재하지만 값은 존재하지 않는 것.

이 때는 두 함수를 합쳐 사용하면 된다.

예를 들어, 이렇게 코드를 짜고 싶으면,

Map<Dish.Type, List<Dish>> caloricDishesByType =
		menu.stream()
        	.filter(dish -> dish.getCalories() > 500)
            .collect(groupingBy(Dish::getType));
// {Other=[french fries, pizza], MEAT=[pork, beef]}

아래처럼 코드를 짜면 된다.

Map<Dish.Type, List<Dish>> caloricDishesByType = 
		menu.stream()
        	.collect(
            	groupingBy(Dish::getType,
                	filtering(dish -> dish.getCalories() > 500,
                    toList()
                )
            );
// {Other=[french fries, pizza], MEAT=[pork, beef], FISH=[]}

flatmapping을 활용해서 간편하게 추출할 수도 있다

Map<Dish.Type, Set<String>> dishNamesByType =
		menu.stream()
        	.collect(
            	groupingBy(
                	Dish::getType,
                    flatMapping(dish -> dishTags.get(dish.getName())
            			.stream(), toSet()
                    )
                )
            );      

🎞️ 다수준 그룹화

Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받는다
이를 다수준으로 이용해 바깥쪽 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.NORAML;
                        else return CaloricLevel.FAT;
                    })
                )
           );
// MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, 
// Fish={DIET=[prawns], NORMAL=[salmon]}
                    

🎞️ 서브그룹으로 데이터 수집

Map<Dish.Type, Optional<Dish>> mostCaloricByType = 
		menu.stream()
        	.collect(
            	groupingBy(
                	Dish::getType, // 분류 함수
                	collectingAndThen(
                		maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
                		Optional::get // 변환 함수
                    )
                )
            );
  • collectingAndThen: 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환하는 팩토리 메서드
  • 반환되는 컬렉터는 기존 컬렉터의 래퍼 역할: collect의 마지막 과정에서 변환 함수로 자신이 반환하는 값을 매핑
    • maxBy로 만들어진 컬렉터가 감싸지는 컬렉터. 변환 함수 Optional::get으로 반환된 Optional에 포함된 값 추출

🎞️ 분할

분할 함수: predicate을 분류 함수로 사용하는 특수한 그룹화 기능

  • boolean 반환함
    • 그래서 맵의 키 형식은 Boolean
  • 그룹화 맵은 최대 두개의 그룹으로 분류
    • 참 아니면 거짓의 값을 가짐
<Map<Boolean, List<Dish>> partitionedMenu = 
		menu.stream()
        	.collect(
            	partitioningBy(Dish::isVegeterian)
             );
// {false = [pork, beef], true[french fries, rice, pizza]}

분할의 장점: 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지함

Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
		menu.stream()
        	.collect(
            	partitioningBy(Dish::isVegeterian, groupingBy(Dish::getType))
            );
// {false={FISH=[prawns, salmon], MEAT=[pork, beef]}, 
// true={OTHER=[french fries, rice, pizza]}}

채식 요리와 채식이 아닌 요리 각각의 그룹에서 가장 칼로리가 높은 요리 찾기

Map<Boolean, Dish> mostCaloricPartitionedByVegeterian = 
		menu.stream()
        	.collect(
            	partitioningBy(Dish::isVegeterian, 
                				collectingAndThen(
                                	maxBy(comparingInt(Dish::getCalories)),
                                    Optional::get)));
// {false=pork, true=pizza}                              

🎞️ Collector 인터페이스

public interface Collector<T, A, R> {
	Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    Function<A, R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
}
  • T - 수집될 스트림 항목의 제네릭 형식
  • A - 누적자. 수집 과정에서 중간 결과를 누적하는 객체의 형식
  • R - 수집 연산 결과 객체의 형식
    • 항상은 아니지만 대개 컬렉션 형식

supplier 메서드: 새로운 결과 컨테이너 만들기

supplier 메서드: 빈 결과로 만들어진 Supplier 반환

  • 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수

accumulator 메서드: 결과 컨테이너에 요소 추가하기

accumulator 메서드 - 리듀싱 연산을 수행하는 함수 반환

  • 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자와 n번째 요소를 함수에 적용
  • 함수의 반환값은 void
    • 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없음

finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기

finisher 메서드: 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환함

  • ToListCollector 등을 사용하면 누적자 객체가 이미 최종 결과인 상황도 존재 > 이 때는 변환 과정이 필요 없어서 finisher 메서드는 항등 함수 반환

combiner 메서드: 두 결과 컨테이너 병합

combiner 메서드: 리듀싱 연산에서 사용할 함수를 반환함

  • 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의

characteristics 메서드

characteristics 메서드: 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합 반환

Characteristics

  • UNORDERED - 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않음
  • CONCURRENT
    • 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있음
    • 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있음
    • 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있음
  • IDENTITY_FINISH
    • finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략 가능
    • 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있음
    • 누적자 A를 결과 R로 안전하게 형변환 가능

참고: Modern Java in Action (라울-게이브리얼 등 지음)

profile
우당탕탕

0개의 댓글