6장: 스트림으로 데이터 수집

컬렉터로 가독성 좋게 데이터를 모아보자.

Collector란?

  • 스트림의 collect 메서드에 Collector 인터페이스 구현을 넘겨 스트림의 요소를 어떤 식으로 도출할지 지정한다.

  • 예를 들어 toList()는 리스트를 반환하도록 하고, groupingBy()는 주어진 키에 대응하는 리스트를 갖는 맵 형태로 반환하도록 한다.

고급 리듀싱 기능 수행

  • 컬렉터의 최대 강점은 collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점이다.

  • 스트림에서 collect를 호출하면 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 자동으로 작업을 처리해준다.

  • 보통 함수를 요소로 변환할 때 컬렉터를 적용하며, 최종 결과를 저장하는 자료구조에 값을 누적한다.

미리 정의해 둔 컬렉터

  • Collectors 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 메서드를 제공한다.

  • Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분되며, 앞으로 이 세 가지를 공부해볼 것이다.

    • 스트림 요소를 하나의 값으로 리듀스 및 요약

      • 리스트에서 총합을 구하는 등의 다양한 연산을 수행한다.

    • 스트림 요소 그룹화

      • 특정 키를 이용해 그룹화하거나 서브 그룹에 추가로 리듀싱 연산을 적용하는 등의 작업이 가능하다.

    • 스트림 요소 분할

      • Predicate를 그룹화 함수로 사용한다.

리듀싱과 요약

  • 컬렉터는 스트림의 모든 항목을 하나의 결과로 합칠 수 있다.

  • 아래는 counting() 팩토리 메서드가 반환하는 컬렉터를 사용해 메뉴의 총 개수를 구하는 코드이다.

import static java.util.stream.Collectors.*;

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

long howManyDishes = menu.stream().count(); //collect 생략 가능

최댓값, 최솟값 찾기

  • 스트림 Collectors.maxBy, Collectors.minBy 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다

  • 두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 입력 인자로 받는다.

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

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

요약 연산

  • 리듀싱 기능을 사용해 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산

  • 합계

    • Collectors.summingInt, summingDouble, summingLong 메서드를 통해 합계를 구하는 요약 연산을 수행하는 Collector를 얻을 수 있다.

    • Collectors.summingInt 메서드는 객체를 int로 매핑하는 함수를 인수로 받고, 인수로 전달된 함수는 객체를 int로 매핑한 컬렉터를 반환한다. 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.

    • 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.

    int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
  • 평균값 계산

    • Collectors.averagingInt, averagingLong, averagingDouble 정적 메서드를 통해 제공된다.

    double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
  • 합계, 평균, 최소값 및 최대값을 한번에 구하기

    • 만약 두개 이상의 연산이 한번에 수행되어야 한다면 Collectors.summarizingInt 정적 메서드가 반환하는 컬렉터를 사용할 수 있다.

    IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
    // menuStatistics : IntSummaryStatistics{count=9, sum=4300, min=120, average=477.778, max=800}

문자열 연결

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

  • 내부적으로 StringBuilder를 이용해 문자열을 하나로 만든다.

  • joining 메서드의 인자로 구분자를 넣을 수 있다.

String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

범용 리듀싱 요약 연산

  • reducing 팩토리 메서드를 사용해 지금까지 다룬 모든 컬렉터를 정의할 수도 있지만, 가독성과 편의성을 위해 특화된 컬렉터를 따로 제공하고 있다.

  • reducing 메서드의 입력 인자

    • 리듀싱 연산의 시작 값 혹은 스트림이 비었을 때의 반환값

    • 변환(매핑) 함수

    • 같은 종류의 두 항목을 더해 하나의 값으로 만드는 BinaryOperator이다.

    public static <T, U> Collector<T, ?, U> reducing(U identity,
                                                     Function<? super T, ? extends U> mapper,
                                                     BinaryOperator<U> op) {
            return new CollectorImpl<>(
                    boxSupplier(identity),
                    (a, t) -> { a[0] = op.apply(a[0], mapper.apply(t)); },
                    (a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
                    a -> a[0], CH_NOID);
    }
  • 다음은 리듀싱을 사용해 칼로리의 합계와 최대 칼로리를 가진 메뉴를 구하는 코드이다.

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

Optional<Dish> mostCaloriesDish = 
    menu.stream().collect(reducing(
        (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
  • collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계되었으나, reduce는 두 값을 하나로 도출하는 불변형 연산이다.

  • 따라서 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 reduce 메서드 대신 collect 메서드를 사용해 리듀싱 연산을 구현해야 한다.

  • 잘 이해가 가지 않으니 7장에 가서 자세히 알아보도록 하자...😓

자신의 상황에 맞는 최적의 해법 선택

  • 함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있다.

  • 컬렉터를 이용하면 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 코드가 복잡해지지만, 재사용성과 커스터마이즈 가능성을 제공하여 높은 수준의 추상화와 일반화를 얻을 수 있다.

  • 모든 메뉴의 칼로리 합계를 구할 때 아래와 같이 다양한 방법을 사용할 수 있다.

// 1) reducing 컬렉터와 Integer 클래스의 sum 메서드 참조를 사용
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum));

// 2) 스트림을 IntStream으로 매핑해 sum 메서드를 호출
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();

그룹화

  • 분류 함수

    • 스트림을 그룹화하는 기준이 되는 함수

    • groupingBy 메서드의 인자로 전달되는 함수가 이에 해당

    • 예를 들어 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수

  • 그룹화 연산의 결과로는 "그룹화 함수가 반환하는 값을 Key로, 그리고 키에 해당하는 스트림 요소의 리스트를 Value로 갖는 맵"이 반환된다.

  • 단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.

  • 다음 코드는 400 칼로리 이하를 'diet', 400~700칼로리를 'normal', 700칼로리 이상을 'fat' 요리로 분류하여 Map 형태로 저장한다.

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;
  }));

그룹화된 요소 조작

필터링

  • 그룹의 요소 중 특정 조건으로 필터링하려면 groupingBy() 팩토리 메서드를 오버로드해 두번째 인자로 filtering() 메서드를 넣어주어야 한다.

  • 미리 조건으로 필터링한 후 groupingBy()를 수행하면 조건에 해당하지 않는 키가 아예 누락되는 문제가 있다.

// 먼저 필터링할 경우 FISH는 미리 필터링되므로 키가 누락된다.
Map<Dish, Type, List<Dish>> caloricDishesByType = menu.stream()
  .filter(dish -> dish.getCalories() > 500)
  .collect(groupingBy(Dish::getType));
// 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef]}

// groupingBy 내부에 filtering Predicate를 넣어 모든 키를 보존할 수 있다.
Map<Dish, Type, List<Dish>> caloricDishesByType = menu.stream()
  .collect(groupingBy(Dish::getType, filtering(dish -> getCalrories() > 500, toList())));
// 결과: {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}

매핑

  • 매핑 함수를 이용해 요소를 반환할 수 있다.

// mapping Predicate 사용해 메뉴 이름만 리스트로 반환
Map<Dish.Type, List<Sting>> dishNamesByTypes = menu.stream()
  .collect(groupingBy(Dish::Type, mapping(Dish::getName, toList())));
  • flatMap 변환 역시 사용 가능하다.

// flatMapping Predicate 사용해 dishTags Map에서 메뉴에 해당하는 태그 리스트들으로부터 Set을 반환
Map<Dish.Type, Set<String>> dishNamesByType = menu.stream()
  .collect(groupingBy(Dish::getType,
    flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));

// 결과: {MEAT=[salty, greasy, roasted, fried, crisp], FISH= [roasted, tasty, fresh], OTHER:[salty, natural, light]}

다수준 그룹화

  • 여러 조건을 기준으로 이중, 삼중맵으로 그룹화할 수 있다.

  • groupingBy 메서드 내부에 두번째 기준을 정의하는 groupingBy 메서드를 전달해 이중맵으로 스트림의 항목을 그룹화할 수 있다.

  • 보통 groupingBy의 연산을 '버킷(물건을 담을 수 있는 양동이)' 개념으로 생각하면 쉽다.

  • 첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채우기를 반복하면 n수준의 그룹화가 가능하다.

  • 다음 코드는 메뉴 종류에 따라 1차 분류한 후, 칼로리 정도에 따라 2차 분류하는 방식이다.

Map<Dish.Type, Map<CalricLevel, 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;
      })
    )
};

//dishesByTypeCaloricLevel : 
//{MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]}, FISH={DIET=[prawns], NORMAL=[salmon]}, OTHER={...}}

서브 그룹으로 데이터 수집

  • 분류 함수 한개만 인수를 갖는 groupingBy(f) 메서드는 사실 groupingBy(f, toList())의 축약형이다.

  • groupingBy 메서드는 두번째 인수로 다양한 컬렉터를 전달받을 수 있어, 이를 이용해 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 수 있다.

  • 아래는 counting 컬렉터를 입력하는 코드이다.

Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
// 결과: {MEAT=3, FISH=2, OTHER=4}
  • Collectors.collectingAndThen 팩토리 메서드는 컬렉터가 반환한 결과를 변환 함수에 적용시켜 다른 컬렉터를 반환한다.

  • 아래와 같이 Optional.get()으로 Optional의 값을 꺼낼 수 있으며, 이 때 리듀싱컬렉터는 절대 Optional.empty()를 반환하지 않으므로 get()을 호출해도 안전하다.

Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream()
  .collect(groupingBy(Dish::getType,
                      collectingAndThen(maxBy(CompaingInt(Dish::getCalories)), 
                                        Optional::get)));
// 결과: {FISH=salmon, OTHER=pizza, MEAT=pork}

여러 컬렉터 예제

  • summingInt 컬렉터를 사용해 메뉴 종류에 따른 칼로리의 합계를 반환한다.

Map<Dish.Type, Integer> totalCaloriesByType = menu.stream()
  .collect(groupingBy(Dish::getType, summingInt(Dish::getCalories)));
  • mapping 컬렉터를 사용해 메뉴 종류에 따른 칼로리 분류 셋을 반환한다.

  • toCollection을 사용해 원하는 객체로 반환하도록 할 수 있다.

Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream()
  .collect(groupingBy(Dish::getType, mapping(dish -> {
    if (dish.getCalories() <= 400) return caloricLevel.DIET;
    else if (dish.getCalories() <= 700) return caloricLevel.NORMAL;
    else return caloricLevel.FAT;
  }, toCollection(HashSet::new))));
//{OTHER=[DIET, NORMAL], MEAT=[DIET, NORMAL, FAT], FISH=[DIET, NORMAL]}

분할

  • 분할 함수를 분류 함수로 사용하는 특수한 그룹화 기능이다.

  • 분할 함수(boolean을 반환하는 Predicate)를 분류 함수로 사용하기 때문에 결과 맵의 키 형식은 Boolean이다.

  • 다음 코드는 채식 메뉴이면 true 그룹에, 채식 메뉴가 아니면 false 그룹에 들어가도록 한다.

Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
                                               .collect(partitioningBy(Dish::isVegetarian));

// {false=[pork, beef, chicken, prawns, salmon], true=[french fires, rice, season fruit, pizza]}

장점

  • 분할 함수가 반환하는 참, 거짓의 스트림 리스트를 모두 유지한다.

  • partitioningBy 메서드의 두번째 인자로 컬렉터를 전달할 수도 있다.

    // groupingBy 컬렉터를 전달해 타입별로 그룹핑
    Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(
      Dish::isVegetarian, groupingBy(Dish::getType));
    // 결과: {false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]}, true={OTHER=[french fires, rice, season fruit, pizza]}}
    
    // maxBy 컬렉터를 전달해 칼로리가 가장 높은 메뉴만 반환
    Map<Boolean, Dish> partitionedMenu = menu.stream().collect(partitioningBy(
      Dish::isVegetarian, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get));
    // 결과: {false=pork, true=pizza}
  • partitioningBy 메서드의 두번째 인자로 partitioningBy 메서드를 중첩할 수도 있다.

    Map<Boolean, Map<Boolean, List<Dish>>> partitionedMenu = 
      menu.stream()
          .collect(partitioningBy(Dish::isVegetarian, 
                   partitioningBy(d -> d.getCalories() > 500)));

소수와 비소수로 분할

  • 정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누어보자.

  • 소수이면 true 그룹에, 비소수이면 false 그룹에 들어가도록 한다.

  • 먼저 소수인지 판별하는 메서드부터 만든다. (특정 수의 제곱근보다 작은 수로 특정 수를 나눌 수 없으면 소수이다.)

public boolean isPrime(int candidate) {
  int candidateRoot = (int) Math.sqrt((double)candidate);
  return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}

public Map<Boolean, List<Integer>> partitionPrimes(int n) {
  return IntStream.rangeClosed(2, n)
                  .boxed()
                  .collect(partitioningBy(candidate -> isPrime(candidate)));
}

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();
}
  • 위 코드는 Collector 인터페이스의 5가지 메서드를 나타낸다.

    • supplier: 새로운 결과 컨테이너를 만든다.

    • accumulator: 결과 컨테이너에 요소를 추가한다.

    • finisher: 최종 변환값을 결과 컨테이너로 적용한다.

    • combiner: 두 결과 컨테이너를 병합한다.

    • characteristics: collect 메서드가 어떤 최적화를 이용해 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공한다.

  • T는 수집될 스트림 항목의 제네릭 형식이다.

  • A는 누적자. 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.

  • R은 수집 연산 결과 객체의 형식(보통 컬렉션 형식)이다.

supplier

  • 새로운 결과 컨테이너를 만들기 위해 빈 결과로 이루어진 Supplier를 반환해야 한다.

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

  • ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.

public Supplier<List<T>> supplier() {
    return ArrayList::new;
}

accumulator

  • 리듀싱 연산을 수행하는 함수를 반환한다.

  • 스트림에서 n번째 요소를 탐색할 때, n-1번째 항목까지 수집한 누적자와 n번째 요소를 함수에 적용한다.

  • 함수의 반환값은 void이고 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로, 누적자가 어떤 값일지 단정할 수 없다.

  • ToListCollector에서 accumulator가 반환하는 함수는 기존 리스트(누적자)에 현재 항목을 추가한다.

public BiConsumer<List<T>, T> accumulator() {
  return List::add;
}

finisher

  • 스트림 탐색을 끝내고 누적자 객체를 최종 객체로 변환할 때 호출할 함수를 반환해야 한다.

  • ToListCollector는 누적자 객체가 최종 결과이기 때문에 변환 과정이 필요없어 항등 함수를 반환한다.

public Function<List<T> List<T>> finisher() {
  return Function.identity();
}

combiner

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

  • toList의 combiner는 간단하게 스트림의 두 번째 서브 파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.

public BinaryOperator<List<T>> combiner() {
  return (list1, list2) -> {
    list1.addAll(list2);
    return list1;
  }
}
  • 스트림의 리듀싱을 병렬로 처리할 수 있도록 해주는 메서드이다.

  • 병렬 리듀싱 수행 과정

    • 스트림을 언제까지 분할할지에 대한 조건에 맞추어 스트림을 재귀적으로 분할해 서브스트림들을 만든다.

    • 모든 서브스트림의 각 요소에 리듀싱 연산을 순차적으로 적용해 병렬로 처리한다.

    • 컬렉터의 combiner 메서드가 반환하는 함수를 사용해 서브스트림의 결과들을 합친다.

Characteristics

  • 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.

  • Characteristics는 스트림을 병렬로 리듀스 할 지 여부와, 병렬로 리듀스 시 선택할 최적화에 대한 힌트를 제공한다.

  • Characteristics의 항목

    • UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.

    • CONCURRENT : 다중 스레드에서 accumulator 함수를 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.

    • IDENTITY_FINISH : 최종 결과로 누적자 객체를 바로 사용하는 경우 finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.

정리

  • 아래와 같이 ToListCollector를 구현할 수 있다.

public class ToListCollector<T> implements Collect<T, List<T>, List<T>> {
  @Override
  public Supplier<List<T>> supplier() {
    return ArrayList::new;
  }
  
  @Override
  public BiConsumer<List<T>, T> accumulator() {
    return List::add;
  }
  
  @Override
  public Function<List<T> List<T>> finisher() {
    return Function.identity();
  }
  
  @Override
  public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
      liat.addAll(list2);
      return list1;
    }
  }
    
  @Override
  public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
  }
}
  • 컬렉터 구현 클래스를 만드는 대신 세 함수(발행, 누적, 합침)를 인수로 받는 collect메서드를 사용할 수 있다.

  • 단, IDENTITY_FINISH 수집 연산일 때 해당된다.

  • 세 함수는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.

List<Dish> dishes = menuStream.collect(
  ArrayList::new, //발행
  List::add,  //누적
  List:addAll); //합침

커스텀 컬렉터

  • 어떤 수가 소수인지 판별하기 위해서는, 소수로만 나누어떨어지는지 확인하면 된다.

  • 일반적으로는 컬렉터 수집 과정에서 부분 결과에 접근할 수 없으므로 커스텀 컬렉터 클래스를 통해 접근할 수 있다.

  • 아래 isPrime메서드에서는 primes 리스트를 입력받도록 했다.

  • 대상의 제곱근보다 큰 소수를 찾으면 검사를 중단하기 위해서는 takeWhile 메서드를 사용한다.

public boolean isPrime(List<Integer> primes, int candidate) {
  int candidateRoot = (int) Math.sqrt((double)candidate);
  return primes.stream()
    .takeWhile(i -> i <= candidateRoot);
    .noneMatch(i -> candidate % i == 0);
}

커스텀 컬렉터 만들기

  • public interface Collector<T, A, R> 에서 T는 스트림 요소의 형식, A는 중간 결과 누적 객체의 형식, R은 collect 연산의 최종 결과 형식을 의미한다.

  • 우리가 만드는 컬렉터는 소수는 key가 true인 리스트에 저장하고, 비소수는 key가 false인 리스트에 저장한다.

public class PrimeNumbersCollector implements Collect<Integer, Map<Boolean, List<Integer>>,
                                                      Map<Boolean, List<Integer>>> {
  // 1) supplier: 누적자 초기화하는 함수 반환
  public Supplier<Map<Boolean, List<Integer>>> supplier() {
    return () -> new HashMap<Boolean, List<Integer>>() {{
      put(true, new ArrayList<Integer>());
      put(false, new ArrayList<Integer>());
    }};
  }
  
  // 2) accmulator: 누적자에 요소 추가하는 함수 반환
  public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
    return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
      acc.get( isPrime(acc.get(true), candidate) ) // 현재까지의 소수 리스트를 isPrime에 넘긴다.
         .add(candidate);
    };
  }
  
  // 3) combiner: 병렬 수행으로 여러 누적자가 생성될 때 합쳐주는 함수 반환
  public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
    return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
      map1.get(true).addAll(map2.get(true));
      map1.get(false).addAll(map2.get(false));
    };
  }
  
  // 4) finisher: 누적자와 결과 형식이 동일하므로 항등함수 반환
  public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
    return Function.identity();
  }
  
  // 5) 커스텀 컬렉터는 CONCURRENT, UNORDERED에는 해당하지 않는다.
  public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
  }
}
  • 커스텀 컬렉터를 아래와 같이 사용할 수 있다.

public Map<Boolean, List<Integer>> partitionPrimes(int n) {
    return IntStream.rangeClosed(2,n)
                    .boxed()
                    .collect(new PrimeNumbersCollector());
}
  • 혹은 collect 메서드 인자로 supplier, accumulator, combiner 함수를 직접 넣어 사용할 수도 있다. (가독성과 재사용성이 떨어지기 때문에 코드는 생략하겠습니다 🙂)

Last updated