Java 8 : Collector 정리

600g (Kim Dong Geun)·2021년 4월 17일
0

Java 8 : Collector

  • 컬렉터란 스트림의 요소를 어떤식으로 도출할지 지정하는 함수.

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

  • 훌륭하게 설계된 함수형 API의 또다른 장점은 높은 수준의 조합성재사용성을 꼽을 수 있다.
  • collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 최대 강점이다.
  • Stream의 Collector를 호출하면 내부적으로 리듀싱 연산이 일어나는 모습을 볼 수 있다.
  • 명령형 프로그래밍에서는 우리가 직접 구현해야했던 작업이 자동으로 수행된다.

미리정의된 컬렉터

이번 장에서는 다음과 같은 요소의 컬렉터를 배우게 된다.

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

리듀싱과 요약

Counting

  • stream 요소의 개수를 반환한다.
long howManyDishes = menu.stream().collect(Collectros.counting());
  • 다음과 같이 불필요한 과정을 생략할 수도 있음
long howManyDishes = menu.stream().conut();

스트림 값에서 최댓값과 최솟값 검색

  • Collectors.maxBy, Collectors.minBy 두개의 메소드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.
Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));

Optional<Dish> leastCaloriesDish = menu.stream().collect(minBy(dishCaloriesCcomparator));

요약 연산

  • Collectors 클래스는 Collectors.summingInt 라는 특별한 요약 메소드를 제공.
  • summingInt는 객체를 int로 매핑하는 함수를 인수로 받는다.
합계 연산
  • 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.
int totalCalroies = menu.stream().collect(summingInt(Dish::getCalories));

Collectors.summingLongCollectors.summingDouble 메소드 또한 같은 방식으로 동작

평균 연산
  • Collectors.averagingInt, averagingLong, averagingDouble 등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.
double avgCalories = menu.stream().collect(avergingInt(Dish::getCalories));
전체 요약연산
  • 팩토리 메소드 summarizingInt가 반환하는 컬렉터를 사용할 수 있다.
  • 예를들어서 다음은 하나의 요약 연산으로 메뉴에있는, 요소 수, 요리의 칼로리 합계, 평균, 최댓값, 최솟값 등을 계산하는 코드다.
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));

그러면 다음과 같은 정보를 수집할 수 있다.

IntSummaryStatistics{count=9, sum=4300, min=120, average= 477.7, max=800}

문자열 연결

  • 컬렉터에 joining 팩토리 메소드를 이용하면 스트림의 각 객체에 toString 메소드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결하여 반환한다.
  • 다음은 메뉴의 모든 요리명을 연결하는 코드다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());

joining 메소드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다. Dish 클래스가 요리명을 반환하는 toString 메소드를 포함하고 있다면 다음 코드에서 보여주는 것처럼 각 요리의 이름을 추출하는 과정을 생략할 수 있다.

String shortMenu = menu.stream().collect(joining());
  • 위 코드 모두 띄어쓰기나 어떠한 splitor 없이 joining 연산을 수행하기 때문에, 문자열을 해석하기 어렵다.
  • 따라서 두 요소 사이에 구분 문자열을 넣을 수 있도록 오버로드된 joining 팩토리 메소드를 사용한다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

범용 리듀싱 요약 연산

  • 지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메소드로도 정의할 수 있다. 즉, 범용 Collectors.reducing으로도 구현할 수 있다.
  • 그럼에도 범용 팩토리 메소드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다.

CollectReduce

Collect와 Reduce , 둘 중 어느 것을 사용해도 원하는 결과를 사용할 수 있었다. 그러나 의미론적인 문제에서 둘의 차이가 발생한다. Collect 는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메소드인 반면, Reduce 는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 일어난다.

즉, reduce 메소드가 누적자로 사용된 리스트를 변환시킨다면, 문법에는 맞을진 모르지만 의미론적으로는 틀린것이 된다.

컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.

  • reducing 컬렉터를 사용한 이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메소드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.
int totalCalories = menu.stream().collect(reducing(0, // 초기값
                                                   Dish::getCalories, //합계함수
                                                   Integer::sum)); //변환함수

그룹화

  • 데이터 집합을 하나이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다.
  • 명령형으로 그룹화를 구현하려면 까다롭고, 할일이 많으며, 에러도 많이 발생한다,
  • 하지만 자바8의 함수형을 사용하면 가독성 있는 한 줄의 코드를 그룹화를 구현할 수 있다.
// 메뉴의 타입에 따라 메뉴를 그룹화 하는 함수.
Map<Dish.Type, List<Dish>> dishesByType =
  meun.stream().collect(groupingBy(Dish::getType));
  • groupingBy 메소드를 통해서 스트림이 그룹화되므로 이를 분류 함수 라고 부른다.
  • 그러나 위 코드는 단순한 속성 접근자 대신 더 복잡한 분류기준이 필요한 상황에서는 메소드 참조를 분류 할 수 없다
  • 예를들어 400칼로리 이하를 diet, 400~700칼로리를 normal로, 700칼로리 초과를 fat 요리로 분류한다고 가정했을때, Dish 클래스에는 이러한 연산에 필요한 메소드가 없으므로 메소드 참조를 분류함수로 사용할 수 없다.
  • 따라서 다음 예제에서 보여주는 것처럼 메소드 참조 대신 람다 표현식으로 필요한 로직을 구현할 수 있다.
public enum CaloricLevel {DIET, NORMAL, 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;
	}));

그룹화된 요소 조작

  • 요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산이 필요하다. 예를들어 500칼로리가 넘는 요리만 필터링 한다고 하자. 다음 코드 처럼 그룹화를 하기 전에 프레디 케이처로 필터를 적용해 문제를 해결할 수 있을 것이라 생각한다.
Map<Dish.Type, List<Dish>> caloricDishesByType =
  menu.stream().filter(dish -> dish.getCalories() > 500)
  .collect(groupingBy(Dish::getType));
  • 위 코드로 문제를 해결할 수 있지만, 단점도 존재한다. 우리의 메뉴 요리는 다음처럼 맵 형태로 되어 있으므로 우리 코드에 위 기능을 사용하려면 맵에 코드를 적용하여야 한다.

  • 그러나 만약 filtering된 요소에 하나의 Key값도 존재하지 않게 된다면, 그 Key값은 Collect된 맵 자체에서 사라지게 된다.

  • 위와같은 문제를 해결하기 위해서는 다음과 같이 해결할 수 있다.

Map<dish.Type, List<Dish>> caloricDishesByType = 
  menu.stream()
  .collect(groupingBy(Dish::getType,
                     filtering(dish -> dish.getCalories() > 500, toList())));

다수준 그룹화

  • 두 인수를 받는 팩토리 메소드 Collectors.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;
          })));

서브 그룹으로 데이터 수집

  • 각 음식 타입에서 가장 높은 칼로리의 음식을 저장하라라는 것은 다음과 같이 정할수있다.
Map<Dish.Type, Optional<<Dish>> mostCaloricByType = 
  menu.stream()
  .collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));

컬렉터 결과를 다른 형식에 적용하기

  • 그룹화 연산에서 맵의 모든 값을 Optional 로 감쌀 필요가 없으므로 Optional을 삭제하여 Dish값으로 변환할 수 있다.
Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
  .collect(groupingBy(Dish::getType, //분류함수
                     collectingAndThen(
                     	maxBy(comparingInt(Dish::getCalories)),// 감싸인 컬렉터
                   	Optional::get //변환함수
                     )));

분할 함수

  • 분할은 분할 함수 불리는 프레디케이트를 분류함수로 사용하는 특수한 그룹화 기능이다.
  • 다음은 채식주의자를 위해 채식요리와 채식이 아닌 요리를 분류하는 예이다.
Map<Boolean, List<Dish>> partitionMenu = menu.stream()
  .collect(partitioningBy(Dish::isVegetarian));

분할의 장점

  • 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 모두 유지한다는 것이 분할의 장점이다.

Collector 인터페이스

  • Collector 인터페이스는 리듀싱 연산을 어떻게 구현할지 제공하는 메소드 집합으로 구성된다.
  • 다음은 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은 수집 연산 결과, 객체의 형식이다.
  • 예를들어 Stream<T>의 모든 요소를 List<T>로 수집하는 ToListCollector<T>라는 클래스를 구현할 수있다.
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>

누적 과정에서 사용되는 갹체가 수집과정의 최종결과로 사용된다.

Supplier 메소드 : 새로운 결과 컨테이너 만들기

  • supplier 메소드는 빈 결과로 이루어진 Supplier를 반환해야 한다.
  • 즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수이다.
public Supplier<List<T>> supplier() {
  return () -> new ArrayList<T>();
}

//혹은 생성자 참조를 전달하는 방법이다.
public Supplier<List<T>> supplier() {
  return ArrayList::new;
}

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

  • accmulator메소드는 리듀싱 연산을 수행하는 함수를 반환한다.
  • 스트림에서 n번째 요소를 탐색할 때, 두 인수, 즉 누적자와 n번째 요소를 함수에 적용한다.
  • 함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
public BiConsumer<List<T>,T> accumulator() {
  return (list, item) -> list.add(item);
}

// 메소드 참조를 이용하면 코드가 더 간결해진다.
public BiConsumer<List<T>,T> accumlator(){
  return List::add;
}

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

  • finish메소드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적과정을 끝낼 때 호출할 함수를 반환해야 한다.
  • 때로는 ToListCollector에서 볼 수 있는 것처럼 누적자 객체가 이미 최종 결과인 상황도 있다. 이런 때는 변환 과정이 필요하지 않기 때문에 finisher 메소드는 항등함수를 반환한다.
public Function<List<T>, List<T>> finisher() {
  return Function.identity();
}

Combiner 메소드 : 두 결과 컨테이너 병합

  • 마지막으로 리듀싱 연산에서 사용할 함수를 반환하는 네 번째 메소드 combiner
  • combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
  • 즉, 스트림의 두번째 서브 파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.
public BinaryOperator<List<T>> combiner(){
  return (list1, list2) -> {
    list1.addAll(list2);
    return list1;
  }
}

Characteristics 메소드

  • characterstics 메소드는 컬렉터의 연산을 정의하는 Characteristics형식의 불변 집합을 반환한다.
  • Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스 한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
  • Characteristics는 다음 세 항목을 포함하는 열거형이다.
    • UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 끼치지 않는다.
    • CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.
    • IDENTITY_FINISH : finisher 메소드가 반환하는 함수는 단순히 identity를 적용할 뿐 이므로 이를 생략할 수 있다. 따라서 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글