[JAVA] Stream Collectors - 1. 리듀싱(reducing)

2022. 10. 13. 15:57 JAVA BASIC/Lambda&Stream(람다와 스트림)

컬렉터란 무엇인가?

Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
명령형 코드에서는 문제 해결하는 과정에서 다중 루프와 조건문을 추가하며 가독성과 유지보수성이 크게 떨어진다. 반면 함수형 프로그래밍에서는 필요한 컬렉터를 쉽게 추가할 수 있다.

고급 리듀싱 기능을 수행하는 컬렉터

collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 최대 장점이다. 스트림에 collect를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.

 

 


내부적으로 리듀싱 연산이 일어나는 모습

명령형 프로그래밍에서는 우리가 직접 구현해야 했던 작업이 자동으로 수행된다. collect에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.
Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다. 커스텀 컬렉터 구현은 나중에 알아보자.

미리 정의된 컬렉터(Collectors의 팩토리 메서드)

Collectors 클래스에서 제공하는 팩토리 메서드의 기능은 크게 세 가지로 구분된다.

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

일단 리듀싱과 요약에 대해 알아보자.

리듀싱과 요약

컬렉터로 스트림의 항목을 컬렉션으로 재구성할 수 있다.

long howManyDishes = menu.stream().collect(Collectors.counting());
long howManyDishes = menu.stream().count();

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

Collectorsr.maxBy, Collectors.minBy 두 개의 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다. 두 메서드는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));

2. 요약 연산

스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용된다. 이러한 연산을 요약(summarization)연산이라 부른다.
Collectors.summingInt를 사용하면 총 합계를 구할 수 있다.

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

칼로리로 매핑된 각 요리의 값을 탐색하면서 초깃값(여기서는 0)으로 설정되어 있는 누적자에 칼로리를 더한다.

 

// 평균값
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories)); 
// 메뉴에 있는 요소 수, 칼로리 합계, 평균, 최댓값, 최솟값 등을 계산해준다.
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
// IntSummaryStatistics{count=9, sum=4300, min-120, average=477, max=800}

3. 문자열 연결

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

menu.stream().map(Dish::getName).collect(joining());
// abcde
menu.stream().map(Dish::getName).collect(joining(", "));
// a, b, c, d, e

 

4. 범용 리듀싱 요약 연산

여태까지 본 모든 컬렉터는 reducing 팩토리 메서드로 정의할 수 있다. (범용 팩토리 메서드 대신 특화된 컬렉터를 사용하는 이유는 프로그래밍적 편의성 때문이다.)

// Collectors.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);

// 특화된 컬렉터 사용
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

확실히 간편하지 않은가? 그래도 개발자라면 범용 리듀싱 연산(Collectors.reducing)을 알아야하지 않을까? 몰라두 되려나
reducing

  • 첫 번째 인수 : 리듀싱 연산의 시작값 혹은 스트림에 인수가 없을 때는 반환 값
  • 두 번째 인수 : stream 요소에서 특정 타입으로의 변환하는 함수 - 변환함수
  • 세 번째 인수 : 연산 방법(BinaryOperator) - 여기서는 두 항목을 하나의 값으로 더하는 연산 수행 - 합계함수

reduce vs collect

Stream 인터페이스의 collect와 reduce 메서드는 무엇이 다른 걸까? 우리는 이 메서드들로 같은 기능을 구현할 수 있다. 그래서 더더욱 헷갈린다.

collect(toList()) 대신 reduce 를 사용하여 리스트를 반환할 수 있다.

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

// 1. reduce
// 병렬 처리 시 각자 다른 쓰레드에서 실행한 결과를 마지막에 합치는 단계
// 병렬 스트림에서만 동작
List<Integer> numbers = stream.reduce(
    new ArrayList<Integer>(),
    (List<Integer> l, Integer e) -> {
        l.add(e);
        return l;
    },
    (List<Integer> l1, List<Integer> l2) -> {
        l1.addAll(l2);
        return l1;
    }
);

// 2. collect
List<Integer> numbers = stream.collect(Collectors.toList());

collect : 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계가 되어있는 메서드
reduce : 두 값을 하나로 도출하는 불변형 연산 하는 메서드
따라서 위의 reduce는 누적자로 사용된 리스트를 변환시키므로 잘못 활용한 예이다.

  • 여러 스레드가 동시에 데이터 구조체(여기서는 list)를 수정하게 되면 리스트 자체가 망가져 병렬 연산 수행 불가
  • 위의 문제를 해결하기 위해 매번 새로운 리스트를 할당해야함 그렇게 된다면 성능 저하

결론 : 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직하다.

자신의 상황에 맞게 골라쓰자

하나의 연산을 다양한 방법으로 해결할 수 있다. 가장 가독성이 좋고 간결한 방법을 사용하자.
아래와 같이 메뉴의 칼로리들의 합을 구하는 스트림 연산을 사용할 수 있다.

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get();
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum(); 
// 마지막 방법이 언방식도 피하고 성능상 가장 좋음. 또한 가독성이 좋고 간결하므로 세번째 방법 추천

출처 : 모던자바인액션

출처: https://cornswrold.tistory.com/546 [평범한개발자노트:티스토리]