본문 바로가기
프로그래밍/JAVA 내용정리

[MordernJavaInAction] 6장 스트림으로 데이터 수집(Stream Collector) 2편.

by 노잼인간이라불립니다 2022. 10. 3.

이 글은 MordernJavaInAction이란 책을 읽고 요약 정리한 글입니다.

 

6장 1편에 이어서 진행되는 글 입니다.

 

- 데이터 분할

Collectors.partitioningBy()

 

분할은 분할 함수(partitioning function) 이라 불리는

Predicate를 분류함수로 사용하는 특수한 그룹화 기능입니다.

분할 함수는 불리언을 반환하므로 맵의 키는 Boolean 타입인

true, false 딱 2종류로만 분할이 됩니다.

(다중 분류는 groupingBy를 사용해야 합니다.)

 

 

- 분할의 장점

참과 거짓 두가지 요소의 스트림 리스트가 모두 유지 됩니다.

(true에 해당하는 데이터가 없더라도 리스트는 유지됨.)

 

그럼 예제코드로 살펴보시죠.

 

/**
 * 6.4 분할 - partitioningBy()
 */
@DisplayName("채식과 채식이 아닌 요리로 partitioning")
@Test
public void classifyDishIsVegetarian() {
    // when
    Map<Boolean, List<Dish>> partitioningDish =
            menu.stream()
                    .collect(partitioningBy(Dish::isVegetarian));

    // then
    System.out.println(partitioningDish);
    /*
     * 결과 값.
     * {
     * false=[pork, beef, chicken, prawns, salmon],
     * true=[french fries, rice, season fruit, pizza]
     * }
     */

    System.out.println(partitioningDish.get(true)); // [french fries, rice, season fruit, pizza]
    System.out.println(partitioningDish.get(false)); // [pork, beef, chicken, prawns, salmon]
}

/**
 * 6.4.1 분할의 장점 - partitioningBy(groupBy())
 */
@DisplayName("채식과 채식이 아닌 요리로 나누고, 요리 타입으로 분류하기")
@Test
public void partitioningAndGroupByDish() {
    // when
    Map<Boolean, Map<Type, List<Dish>>> vegetarianDishGroupbyType =
            menu.stream()
                    .collect(
                            partitioningBy(Dish::isVegetarian,
                                    groupingBy(Dish::getType)));

    // then
    System.out.println(vegetarianDishGroupbyType);
    /*
     * {
     * false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]},
     * true={OTHER=[french fries, rice, season fruit, pizza]}
     * }
     */
}

/**
 * 6.4.1 분할의 장점 - partitioningBy(maxBy())
 */
@DisplayName(" 채식 요리인지 분류 후 최대 칼로리 값인 요리 찾기")
@Test
public void maxCalOfVegetarianDish() {
    // when
    Map<Boolean, Dish> maxCalOfVegetarianDish =
            menu.stream()
                    .collect(
                            partitioningBy(Dish::isVegetarian,
                                    collectingAndThen(maxBy(Comparator.comparing(Dish::getCalories)), Optional::get)));

    // then
    System.out.println(maxCalOfVegetarianDish); // {false=pork, true=pizza}
}

 

- 숫자를 소수와 비소수로 분리하기.

 

Collectors.partitioningBy를 이용해 소수와 비소수를 나누어 볼 수도 있습니다.

 

/**
 * 6.4.2 숫자를 소수와 비소수로 분할하기. - IsPrime을 이용하여 2~100까지 소수와 비소수로 나누어보기
 */
@DisplayName("2~100 사이 소수와 비소수로 나누어 보기")
@Test
public void classifyPrimeAndNonPrime() {
    // when
    Map<Boolean, List<Integer>> classifiedNumber =
            IntStream.rangeClosed(2, 100).boxed()
                    .collect(partitioningBy(IsPrime::isPrime));

    // then
    System.out.println(classifiedNumber);
    /* 결과 값
     * {
     * false=[4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 22, 24, 25, 26, 27, 28, 30, 32, 33, 34, 35, 36, 38, 39, 40, 42, 44, 45, 46, 48, 49, 50, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 65, 66, 68, 69, 70, 72, 74, 75, 76, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 90, 91, 92, 93, 94, 95, 96, 98, 99, 100],
     * true=[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
     * }
     */
}

 

- Collector 인터페이스

 

지금 까지는 Collectors가 제공하는 정적 팩토리 메서드만을 살펴보았습니다.

그러나 추가적으로 Collectors를 사용자의 입맛에 맞게 커스텀하여 사용할 수도 있습니다.

 

그럼 Collect 인터페이스에 대해서 살펴보도록 하죠.

 

컬렉터 인터페이스

public interface Collector<T, A, R> {
    /**
      * 새로운 변경 가능한 결과 컨테이너를 생성하고 반환하는 함수.
      *
      * @return 새롭고 변경 가능한 결과 컨테이너를 반환하는 함수
      */
    Supplier<A> supplier();

    /**
      * 값을 변경 가능한 결과 컨테이너로 접는 함수.
      *
      * @return 값을 변경 가능한 결과 컨테이너로 접는 함수
      */
    BiConsumer<A, T> accumulator();

    /**
      * 두 개의 부분 결과를 받아 병합하는 함수. 그만큼
      * Combiner 함수는 한 인수에서 다른 인수로 상태를 접을 수 있으며
      * 그것을 반환하거나 새로운 결과 컨테이너를 반환할 수 있습니다.
      *
      * @return 두 개의 부분 결과를 결합된 결과로 결합하는 함수
      * 결과
      */
    BinaryOperator<A> combiner();

     /**
      * 중간 누적형에서 최종 변환 수행
      * {@code A}를 최종 결과 유형 {@code R}로.
      *
      * <p>특성 {@code IDENTITY_FINISH}가
      * set, 이 함수는
      * {@code A}에서 {@code R}로의 선택되지 않은 캐스트.
      *
      * @return 중간 결과를 최종 결과로 변환하는 함수
      * 결과
      */
    Function<A, R> finisher();

    /**
      * 다음을 나타내는 {@code Collector.Characteristics}의 {@code Set}를 반환합니다.
      * 이 수집가의 특징. 이 집합은 변경할 수 없습니다.
      *
      * @return 컬렉터 특성의 불변 집합
      */
    Set<Characteristics> characteristics();

 

- 설명-

T는 수집될 스트림 항목의 제네릭 타입입니다. - Type

A는 수집과정에서 중간결과를 누적하는 객체의 타입입니다 - Accumulater

R은 최종 결과의 객체 타입입니다. - Result

 

1. supplier 메서드 

새로운 변경 가능한 결과 컨테이너를 생성하고 반환하는 함수.

(A function that creates and returns a new mutable result container.)

 

가변 컨테이너를 생성하고 반환하는 메서드 입니다.

 

2. accoumulator 메서드

값을 변경 가능한 결과 컨테이너로 접는 함수.

(A function that folds a value into a mutable result container.)

 

folds 라는 단어가 사용되었는데 아마 리듀싱 연산을 저렇게 표현한 것 같습니다.

리듀싱 연산을 통해 값을 누적자에 계속 누적합니다.

(n-1번째 까지 값을 수집한 누적자와 n번째 값을 folds 한다.)

 

3. combiner 메서드

두 개의 부분 결과를 받아 병합하는 함수.

(A function that accepts two partial results and merges them.)

 

combiner는 데이터가 순서가 상관없는 경우,

즉, 병렬처리 하도록 특성에 set 되어 있을 경우 실행되는 메서드로

병렬처리 된 누적자 끼리의 연산을 어떻게 처리할지 정의하는 메소드 입니다.

ex) list1.addAll(list2)

 

4. finisher 메서드

중간 누적형에서 최종 변환 수행
      {@code A}를 최종 결과 유형 {@code R}로.
      (Perform the final transformation from the intermediate accumulation type
     {@code A} to the final result type {@code R}.)

 

중간 누적자를 최종 결과값으로 변환하는 메서드입니다.

만약 characteristic의 IDENTITY_FINISH가 SET되어 있다면, 

누적자를 바로 결과 값으로 반환하게 됩니다.

(unchecked cast from {@code A} to {@code R}.)

 

5. characteristics 메서드

다음을 나타내는 {@code Collector.Characteristics}의 {@code Set}를 반환합니다.
      이 수집가의 특징. 이 집합은 변경할 수 없습니다.
      @return 컬렉터 특성의 불변 집합

 

직역하자면 이 인터페이스의 특성을 저장해두는 SET이라고 볼 수 있겠습니다.

Collector는 characteristics메서드에서 반환하는 immutable한 set을 바탕으로

자신이 처리해야 할 것들에 대한 설정값을 알게 되는 것 같습니다.

 

설정값으로는 UNORDERED, CONCURRENT, IDENTITY_FINISH가 있습니다.

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

CONCURRENT - 다중 스레드에서 accumulator 함수를 동시호출 가능.

즉, 병렬로 실행 되어도 되는 지에 대한 설정인 것 같습니다.

IDENTITY_FINISH - 위의 finisher 메서드에서 참조하는 값으로

누적자를 바로 결과값으로 리턴할 것 인가에 대한 특성 설정 값인 것 같습니다.

 

 

마지막으로 커스텀 컬렉터를 구현한 코드를 살펴보고 마무리 하도록 하겠습니다.

 

- 커스텀 컬렉터 구현하여 소수, 비소수 분할하기.

 

-커스텀 컬렉터 구현 클래스-

public class PrimeNumbersCollector
        implements Collector<Integer,Map<Boolean,List<Integer>>, Map<Boolean, List<Integer>>> {
    /**
     * 누적자를 만드는 함수를 반환
     * @return
     */
    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
      return ()-> new HashMap<Boolean,List<Integer>>(){{
              put(true, new ArrayList<Integer>());
              put(false, new ArrayList<Integer>());
          }
      };

    }

    /**
     * true인것은 true에 누적, false 인것은 false에 누적.
     *
     * @return
     */
    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (Map<Boolean, List<Integer>> acc, Integer candidate) ->{
            acc.get( isPrime(acc.get(true),candidate) )
                    .add(candidate);
        };
    }

    /**
     * 병렬과정에 누적자끼리 더하는 방법
     *
     * @return
     */
    @Override
    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));
            return map1;
        };
    }

    /**
     * 변환 과정이 필요 없으므로 항등함수 identity를 반환하도록 메서드 구현
     * @return
     */
    @Override
    public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
        return Function.identity();
    }

    /**
     * 커스텀 컬렉터는 CONCURRENT도 아니고 UNORDERED도 아니지만 IDENTITY_FINISH이므로 아래와 같이 구현.
     * UNORDERED - 스트림의 순서가 의미없는 경우.
     * CONCURRENT - 순서가 상관없는 경우 병렬 실행.
     * IDENTITY_FINISHER - 누적자와 결과타입이 같을경우 누적자를 바로 가져다 쓰게 끔 한다.- 속도향상.
    *
     * @return
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
    }
}

 

 

-실행코드-

/**
     * 예제 6-6 n 이하의자연수를 소수와 비소수로 분류하기 -
     */
    @DisplayName(" n 이하의 자연수를 소수와 비소수로 분류하기")
    @Test
    public void classifyPrimeNumber(){

        // given
        int n= 1_000_000;

        // when
        Map<Boolean, List<Integer>> result =
                IntStream.rangeClosed(2, n).boxed()
                        .collect(partitioningBy(number -> IsPrime.isPrime(number)));

        // then
//        System.out.println(result);
        /* 결과 값
         * {
         * false=[4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20],
         * true=[2, 3, 5, 7, 11, 13, 17, 19]
         * }
         */
    }

    /**
     * 6.6.1 소수로만 나누기
     */
    @DisplayName("커스텀 컬렉터를 이용한 소수판별")
    @Test
    public void partitionPrimesWithCustomCollector(){
        // given
        int n = 1_000_000;

        // when
        Map<Boolean, List<Integer>> result =
                IntStream.rangeClosed(2, n).boxed()
                        .collect(new PrimeNumbersCollector());

        // then
//        System.out.println(result);
        /*
         * {
         * false=[4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20],
         * true=[2, 3, 5, 7, 11, 13, 17, 19]
         * }
         */
    }

    /**
     * 6.6.2 컬렉터 성능 비교
     */
    @DisplayName("컬렉터 성능비교")
    @Test
    public void collectorHarness(){

        long fastest = Long.MAX_VALUE;

        for(int i=0; i<10; i++){
            long start =System.nanoTime();
            partitionPrimesWithCustomCollector(); // 커스텀 컬렉터를 이용한 소수분리 - 81 msecs
//            classifyPrimeNumber(); // 커스텀 하지 않은 상태에서 소수 분리 -  124 msecs
            long duration = (System.nanoTime() - start) / 1_000_000;
            if(duration < fastest) fastest =duration;
        }

        System.out.println("Fastest Execution done in " +fastest + " msecs");

    }

 

 

지금 까지 6장의 주된 내용인 컬렉터와 그에 대한 정적 팩토리 메서드에 대해서 살펴 보았습니다.

 

개인적으로 책을 읽으며 지루한 부분도 없지 않아 있었지만, 마지막 커스텀 컬렉터를 직접 만들어보면서

 

컬렉터 인터페이스에 대해서 살펴보고, 구현해보았는데,

 

그 부분을 진행하면서 좀 더 컬렉터와 스트림에 대해서

 

다시 한번 살펴보고, 이해할 수 있는 계기가 되었던 것 같습니다.

 

그럼 7장에서 뵙겠습니다.

 

 

 

참고

모던 자바인 액션