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

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

by 노잼인간이라불립니다 2022. 9. 25.

이 글은 ModernJavaInAction 이란 책을 읽고 정리한 글 입니다.

 

안녕하세요. 저번 글에서는 스트림의 활용에 대해서 알아 보았는데요.

 

이번글에서는 스트림의 Collectors의 대해 알아보고자 합니다.

 

스트림에 대해서 궁금하신 분들은 이전 글들을 참고해주세요~

https://jojoplot2.tistory.com/entry/JavaInAction-%EC%8A%A4%ED%8A%B8%EB%A6%BCStream

 

[ModernJavaInAction] 4장 스트림(Stream)

이 글은 모던 자바 인 액션이라는 책을 읽고 스스로 내용을 정리하여 작성 한 글입니다. 모든 실습 코드는 아래 주소에 있습니다. https://github.com/jojojojocho/mordernjavainaction GitHub - jojojojocho/mord..

jojoplot2.tistory.com

https://jojoplot2.tistory.com/entry/JavaInAction-%EC%8A%A4%ED%8A%B8%EB%A6%BC%EC%9D%98-%ED%99%9C%EC%9A%A9

 

[ModernJavaInAction] 5장 스트림의 활용

안녕하세요. 저번 글에서는 스트림에 대해서 알아 보았는데요. 이번글에서는 스트림의 활용에 대해 알아보고자 합니다. 스트림에 대해서 궁금하신 분들은 아래 링크에서 읽어보시길 바랄게요~

jojoplot2.tistory.com

 

 

모든 실습 코드는 아래 주소에 있습니다.

https://github.com/jojojojocho/mordernjavainaction

 

GitHub - jojojojocho/mordernjavainaction: 모던자바인액션 연습코드

모던자바인액션 연습코드. Contribute to jojojojocho/mordernjavainaction development by creating an account on GitHub.

github.com

 

이번 시간에는 스트림을 이용한 데이터 수집(Collector)에 대해 알아보고자 합니다.

그럼 출발 해볼까요?

 


스트림으로 데이터 수집

 

- 자바 8부터 새롭게 등장한 스트림은 데이터의 집합을 처리하는 " 게으른 반복자" 라고 할 수 있습니다.

 

이전 글에서 설명 드린 스트림의 특성 중 하나인 Lazyness를 잊지 않으셨겠죠?

 

다시 한번 더 설명해 드리자면 스트림은 중간연산까지 까지는 아무런 결과값이 도출되지 않고,

최종연산 때 모든 계산이 진행된다는 특성을 가지고 있었습니다.

즉, 스트림은 Lazy하게 작동된다는 이야기였습니다.

 

중간연산, 최종연산이 잘 기억나지 않는다고요?

 

그럼 다시 한번 정리해보죠.

 

중간 연산

- 스트림을 다른 연산으로 변환하는 연산.

- 여러 연산을 연결. (파이프 라이닝)

- 스트림의 요소를 소비하지 않는다.

 

최종연산

- 스트림을 소비해서 최종결과를 도출.

- 최종연산은 스트림 파이프라인을 최적하면서 계산과정을 짧게 생략.(쇼트서킷)

 

이제 다들 기억이 나시나요?

 

 

그럼 이번 장에서 헷갈릴 수 있는 개념에 대해서 정리해보고 시작할까요?

 

- 컬렉션(Collection) : 자바에서 제공해주는 컬렉션들을 의미합니다.

- 컬렉터 : 스트림의 최종연산 collect()의 안에 들어가는 Collector 인터페이스를 의미합니다.

- collect : 스트림의 최종연산을 의미합니다.

 

이 정도로 정리해 볼 수 있겠습니다.

 

이전에 배웠던 내용들을 한 가지 더 복기해보자면...

 

개발자가 직접 구현해야하는 명령형 프로그래밍과 

스트림이 제공하는 함수형 프로그래밍의 차이점에 대해서 기억하시나요?

 

함수형 프로그래밍은 how가 아닌 what. 즉, '무엇'을 원하는지 명시하기만 하면 됩니다.

즉 어떻게 처리할 것인가를 신경쓰지 않아도 되죠.

 

또한 Multi Level 그룹화를 하는 경우에

명령형 코드는 수많은 for문과 if문으로 인해 가독성이 떨어지고 유지보수 비용이 커지게 됩니다.

함수형 코드는 필요한 컬렉터를 통해 쉽게 해결할 수 있습니다.

이로 인해 가독성과 유지보수 측면에서 명령형 코드보다 이점을 얻을 수 있습니다.

 

스트림은 이러한 함수형코드의 장점을 가지고 있는 api입니다.

 

그렇다면 이렇게 설계된 함수형 api의 장점은 무엇일까요?

 

1. 여러가지 조합이 가능하다. (사용자의 입 맛대로  api를 조합해서 사용할 수 있습니다.)

2. 재사용성 (어떠한 데이터든 api를 호출하기만하면 같은방식으로 처리가 가능합니다.)

3. 유연하다. (사용자의 입 맛에 따라 정의 하고 싶은 방식 대로 api를 호출하여 사용이 가능합니다.)

 

특히 유연성을 매우 증대시켜주는 Collectors 유틸리티 클래스는

자주 사용하는 컬렉터 인스턴스를 사용자가 사용하기 쉽게 정적 팩토리 메서드(static method)를 제공합니다.

 

그럼 본격적으로 Collectors에 대해 알아 볼까요?

 

Collectors에서 제공하는 메서드의 기능은 크게 3가지 입니다.

1. 스트림 요소를 하나의 값으로 리듀스하고 요약.(maxBy, minBy, summingInt...)

2. 요소 그룹화(groupingBy)

3. 요소 분할 (partitioningBy)

 

1. 리듀싱과 요약

 

1-1. 스트림 에서 최댓값과 최솟값 구하기.

- 스트림에 있는 객체의 숫자필드의 합계나 평균등을 반환하는 연산에도 리듀싱 기능이 자주 사용됩니다.

이러한 연산을 요약(summarization) 연산 이라고 부릅니다.

 

요약연산 예제를 살펴 볼까요??

/**
 * 6.2.1 스트림 값에서 최댓값과 최솟값 검색. - Collectors.maxBy()
 */
@DisplayName("칼로리가 제일 높은 요리 구하기.")
@Test
public void findMaxCaloriesOfDish() {
    // given
    Comparator<Dish> caloriesComparator = Comparator.comparingInt(Dish::getCalories);

    // when
    Dish dishOfMaxCal =
            menu.stream()
                    .collect(maxBy(caloriesComparator)).
                    orElseThrow();

    // then
    System.out.println(dishOfMaxCal); // pork
}

- menu에서 칼로리가 최대인 요리를 구하는 예제 입니다. - 

 

/**
 * 6.2.1 스트림 값에서 최댓값과 최솟값 검색. - Collectors.minBy()
 */
@DisplayName("칼로리가 제일 낮은 요리 구하기")
@Test
public void findMinCaloriesOfDish() {
    // given
    Comparator<Dish> caloriesComparator = Comparator.comparingInt(Dish::getCalories);

    // when
    Dish minOfDishCal =
            menu.stream()
                    .collect(minBy(caloriesComparator)).orElseThrow();

    // then
    System.out.println(minOfDishCal); // season fruit
}

 

- menu에서 칼로리가 최소인 요리를 구하는 예제 입니다. -

 

 

maxBy, minBy 말고도 요약연산에는

summingInt(), summingLong(), summingDouble(),

averagingInt(), averagingLong(), averagingDouble()

summarizingInt(), summarizingLong(), summarizingDouble()

등 이 있습니다.

 

두개 이상의 연산을 한번에 수행하는 경우에는 summarizing이 굉장히 유용합니다.

summarizing에서 제공해주는 항목들은 다음과 같습니다.

1. count

2. sum

3. min

4. average

5. max 

의 항목이 제공됩니다. 

 

그럼 코드 예제들을 한번 살펴 볼까요?

 

/**
 * 6.2.2 요약 연산 - Collectors.summingInt()
 */
@DisplayName("요리의 칼로리 총합 구하기 (Integer)")
@Test
public void calculateSumOfDishCaloriesInt() {
    // when
    Integer sumOfDishCal =
            menu.stream()
                    .collect(summingInt(Dish::getCalories));

    // then
    System.out.println(sumOfDishCal); // 4200
    Assertions.assertThat(sumOfDishCal).isEqualTo(4200);
}

/**
 * 6.2.2 요약 연산 - Collectors.SummingLong()
 */
@DisplayName("요리의 칼로리 총합 구하기 (Long)")
@Test
public void calculateSumOfDishCaloriesLong() {
    // when
    Long sumOfDishCal =
            menu.stream()
                    .collect(summingLong(Dish::getCalories));

    // then
    System.out.println(sumOfDishCal); // 4200
}

/**
 * 6.2.2 요약 연산 - Collectors.SummingDouble
 */
@DisplayName("요리의 칼로리 총합 구하기(Double)")
@Test
public void calculateSumOfDishCaloriesDouble() {
    // when
    Double sumOfDishCal =
            menu.stream()
                    .collect(summingDouble(Dish::getCalories));

    // then
    System.out.println(sumOfDishCal); // 4200.0

}

/**
 * 6.2.2 요약 연산 - Collectors.averagingInt()
 */
@DisplayName("요리의 칼로리 평균 구하기(Int, Long, Double)")
@Test
public void calculateAvgOfDishCalories() {
    // when
    Double avgOfDishCalInt = menu.stream().collect(averagingInt(Dish::getCalories));
    Double avgOfDishCalLong = menu.stream().collect(averagingLong(Dish::getCalories));
    Double avgOfDishCalDouble = menu.stream().collect(averagingDouble(Dish::getCalories));

    // then
    System.out.println(avgOfDishCalInt); // 466.6666666666667
    System.out.println(avgOfDishCalLong); // 466.6666666666667
    System.out.println(avgOfDishCalDouble); // 466.6666666666667
}

/**
 * 6.2.2 요약 연산 - Collectors.summarizingInt()
 */
@DisplayName("두 개 이상의 연산을 한번에 수행")
@Test
public void calculateSummaryOfDishCalories() {
    // when
    IntSummaryStatistics summaryInt = menu.stream().collect(summarizingInt(Dish::getCalories));
    LongSummaryStatistics summaryLong = menu.stream().collect(summarizingLong(Dish::getCalories));
    DoubleSummaryStatistics summaryDouble = menu.stream().collect(summarizingDouble(Dish::getCalories));

    // then
    System.out.println(summaryInt); // IntSummaryStatistics{count=9, sum=4200, min=120, average=466.666667, max=800}
    System.out.println(summaryInt.getMax()); // 800
    System.out.println(summaryInt.getMin()); // 120
    System.out.println(summaryInt.getAverage()); // 466.6666666666667
    System.out.println(summaryInt.getSum()); // 4200
    System.out.println(summaryInt.getCount()); // 9

    System.out.println(summaryLong); // LongSummaryStatistics{count=9, sum=4200, min=120, average=466.666667, max=800}
    System.out.println(summaryDouble); // DoubleSummaryStatistics{count=9, sum=4200.000000, min=120.000000, average=466.666667, max=800.000000}
}

 

문자열 연결 - Joining()

이 뿐만 아니라 Collectors 메서드 중에서는 문자열을 연결시켜주는 메서드도 존재합니다.

 

바로 joining() 인데요. joining 메서드는 내부적으로 Stringbuilder를 이용해 문자열을 하나로 만든다고 합니다.

 

예제 코드로 살펴보시죠.

 

/**
 * 6.2.3 문자열 연결 - Collectors.joining()
 */
@DisplayName("모든 요리의 이름을 연결하기.")
@Test
public void joiningDishNames() {
    /*
     * joining 메서드는 내부적으로 StringBuiler를 이용하여 문자열을 하나로 만듬.
     * StringBuiler - 단일 쓰레드에서 성능 최고. => Not Thread Safe(동기화 x)
     * StringBuffer - 멀티 쓰레드 환경에서 Thread Safe(동기화 o)

     * 정리
     * String - 문자열 연산이 적고 멀티쓰레드 환경일 경우 사용하자!
     * StringBuffer - 문자열 연산이 많고 멀티쓰레드 환경일 경우 사용하자!
     * StringBuilder - 문자열 연산이 많고 단일쓰레드이거나 동기화를 고려하지 않아도 되는 경우 사용하자!
     */
    String joiningDishNames =
            menu.stream()
                    .map(dish -> dish.getName().replaceAll(" ", ""))
                    .collect(joining(",", "JoinedName : ", ""));

    // then
    System.out.println(joiningDishNames); // JoinedName : pork,beef,chicken,french_fries,rice,seasonfruit,pizza,prawns,salmon
}

 

범용 리듀싱 요약 연산 - reducing()

최종연산 중에 reduce가 있다는 사실 잊지 않으셨죠?

이러한 reduce와 같은 동작을 하는 reducing을 Collectors에서도 제공 합니다.

 

collect를 통해 하는 reduce연산과 최종연산의 reduce연산에 대해 알아 볼까요?

 

- collect 메서드는 도출할려는 결과를 누적하는 특성을 가지고 있습니다.

즉, 누적될 때마다 컨테이너 안에 데이터가 추가 되면서 컨테이너의 내용이 변화합니다. - mutable

- reduce 메서드는 두 값을 하나로 도출하는 불변형 연산입니다. - immutable

 

즉, 이미 컨테이너 내용이 변화하도록 구성되있는

collect안에서 reducing을 수행하는 것이 threadsafe하다는 결론입니다.

 

예제코드를 살펴보죠.

/**
 * 6.2.4 범용 리듀싱 요약 연산. - Collectors.reducing()
 */
@DisplayName("한개의 인수를 가진 reducing 이용하기.")
@Test
public void findDishOfMaxCalories() {
    // when
    Dish dishOfMaxCal =
            menu.stream()
                    .collect(reducing(((dish, dish2) -> dish.getCalories() > dish2.getCalories() ? dish : dish2)))
                    .orElseThrow();

    // then
    System.out.println(dishOfMaxCal); // pork
}

 

reducing이 필요한 경우 다양항 방법으로 문제를 해결 할 수 있습니다.

 

예제코드

/**
 * 6.2.4 범용 리듀싱 요약 연산. - Collectors.reducing()
 * 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
 * 메뉴 칼로리의 합계 구하기.
 */
@DisplayName("The sum of calories of the menu.")
@Test
public void calculateTotalCalOfMenu() {
    // when
    Integer totalCaloriesOfMenu =
            menu.stream()
                    .collect(
                            reducing(0,
                                    Dish::getCalories,
                                    Integer::sum));

    // then
    System.out.println(totalCaloriesOfMenu); // 4200
    Assertions.assertThat(totalCaloriesOfMenu).isEqualTo(4200);
}

/**
 * 6.2.4 범용 리듀싱 요약 연산 - stream().map().reduce()
 * 메뉴 칼로리의 합계 구하기.
 */
@DisplayName("the sum of calories of the menu")
@Test
public void calculateSumOfCalOfMenuUsingMapAndReduce() {
    // when
    Integer sumOfCalOfMenu =
            menu.stream()
                    .map(Dish::getCalories)
                    .reduce(Integer::sum)
                    .orElse(0);
    // then
    System.out.println(sumOfCalOfMenu); // 4200
    Assertions.assertThat(sumOfCalOfMenu).isEqualTo(4200);
}

/**
 * 6.2.4 범용 리듀싱 요약 연산 - stream().mapToInt().sum()
 * 메뉴 칼로리의 합계 구하기.
 */
@DisplayName("The sum of calories of the menu")
@Test
public void calculateSumOfCalOfMenu() {
    // when
    int sumOfCalOfMenu =
            menu.stream()
                    .mapToInt(Dish::getCalories)
                    .sum();

    // then
    System.out.println(sumOfCalOfMenu); // 4200
    Assertions.assertThat(sumOfCalOfMenu).isEqualTo(4200);
}

예제 코드 결론 : intStream을 사용한 방법이 가독성과 성능 모두 우수하다. (박싱, 언박싱 비용이 없다.)

 

그리고

 

"스트림 인터페이스에서 직접 제공하는 메서드 보다 컬렉터를 이용하는 것이 복잡하지만,

 

재사용성과 커스터마이징가능성을 제공하는 높은 추상화와 일반화를 얻을 수 있다." 라고 책에 쓰여져 있지만,

 

아직까지는 잘 체감하기 힘든 부분이 있습니다.

 

오히려 map().reduce()를 사용하는게 가독성이나 코드를 짜는 측면에서 편리하다고 아직까지는 생각하고 있습니다.

 

- 그룹화 (groupingBy())

그룹화란?

데이터들을 하나 이상의 카테고리로 분류 하는 것.

이라고 정의할 수 있을 것 같습니다.

 

.collect(groupBy(분류함수))

위와 같은 식으로 작성을 하게 되면 분류함수를 기준으로 그룹화가 이루어 집니다.

 

예제코드

/**
 * 6.3 그룹화 - Collectors.groupBy()
 */
@DisplayName("같은 타입으로 그룹화하기. - 메서드 참조")
@Test
public void classifyByDishTypeRefMethod() {

    // when
    Map<Type, List<Dish>> classifiedMapByDishType =
            menu.stream().
                    collect(
                            groupingBy(Dish::getType));

    // then
    System.out.println(classifiedMapByDishType.entrySet()); //
    /* 결과 값
     * [
     * OTHER=[french_fries, rice, season fruit, pizza],
     * MEAT=[pork, beef, chicken], FISH=[prawns, salmon]
     * ]
     */
}

- 그룹화 된 요소들에 조건 적용하기

이미 그룹화 된 데이터에 함수를 적용할 수도 있습니다. (filltering, mapping, flatmapping 등)

 

예제코드로 살펴보시죠.

/**
 * 6.3.1 그룹화된 요소 조작. - Collectors.groupingBy(), Collectors.filtering()
 */
@DisplayName("칼로리가 500이상인 요리들만 요리타입 기준으로 그룹화 하기 - (모든 타입 나오게)")
@Test
public void groupByDishTypeOver500CaloriesDish() {
    // when
    Map<Type, List<Dish>> over500CalDishGroupByMenuType =
            menu.stream().
                    collect(
                            groupingBy(Dish::getType,
                                    filtering(dish -> dish.getCalories() > 500, toList())));

    // then
    System.out.println(over500CalDishGroupByMenuType); 
    // {OTHER=[french_fries, pizza], MEAT=[pork, beef], FISH=[]}
}

/**
 * 6.3.1 그룹화된 요소 조작 - Collectors.groupBy(), Collectors.mapping()
 */
@DisplayName("요리 타입 기준으로 그룹화 해서 요리 이름으로 매핑")
@Test
public void groupByDishTypeAndMappingDishName() {
    // when
    Map<Type, List<String>> dishNameGroupByDishType =
            menu.stream().collect(
                    groupingBy(Dish::getType,
                            mapping(Dish::getName, toList())));

    // then
    System.out.println(dishNameGroupByDishType);

    /* 결과 값
     * {
     *  OTHER=[french_fries, rice, season fruit, pizza],
     *  MEAT=[pork, beef, chicken],
     *  FISH=[prawns, salmon]
     *  }
     */
}

/**
 * 6.3.1 그룹화된 요소 조작 - Collectors.groupBy(), Collectors.flatMapping()
 */
@DisplayName("flatMapping을 이용한 요리 태그 추출")
@Test
public void groupByDishTagUsingFlatMapping() {
    // given
    Map<String, List<String>> dishTags = new HashMap<>();
    dishTags.put("pork", Arrays.asList("greasy", "salty"));
    dishTags.put("beef", Arrays.asList("salty", "roasted"));
    dishTags.put("chicken", Arrays.asList("fried", "crisp"));
    dishTags.put("french fries", Arrays.asList("greasy", "fried"));
    dishTags.put("rice", Arrays.asList("light", "natural"));
    dishTags.put("season fruit", Arrays.asList("fresh", "natural"));
    dishTags.put("pizza", Arrays.asList("tasty", "salty"));
    dishTags.put("prawns", Arrays.asList("tasty", "roasted"));
    dishTags.put("salmon", Arrays.asList("delicious", "fresh"));

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

    // then
    System.out.println(collect);

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

 

- 다수준 그룹화

 

Collect 안에 groupBy를 2번 사용하여 다수준 그룹화도 가능합니다.

collect(collectors.groupingBy(분류함수,Collectors.groupBy(분류함수)))

 

예제코드로 살펴보시죠.

/**
 * 6.3.2 다수준 그룹화 - groupBy(분류 함수,groupBy(분류 함수))
 */
@DisplayName("요리를 다수준으로 그룹화")
@Test
public void multiLevelGroupByDish() {
    // when
    Map<Type, Map<CaloricLevel, List<Dish>>> multiGroupByDish =
            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;
                                        }
                                    })));

    // then
    System.out.println(multiGroupByDish);
    /*  결과 값.
     * {
     * MEAT={DIET=[chicken], NORMAL=[beef], FAT=[pork]},
     * FISH={NORMAL=[salmon], DIET=[prawns]},
     * OTHER={DIET=[rice, season fruit], NORMAL=[french fries, pizza]}
     * }
     */
}

 

- 서브그룹으로 데이터 수집

 

한개의 인수를 가지는 Collectors.groupingBy(분류함수) 도

사실은 Collectors.groupingBy(분류함수, toList())의 축약형 입니다.

즉, 2번째 자리에 분류함수로 분류된 데이터를 수집할 컬렉터를 선언할 수 있습니다.

2번째 자리에 들어갈 컬렉터의 형식은 제한이 없어 매우 유연하다고 볼 수 있습니다.

ex)

counting(),

summingInt(),

summarizingInt(),

maxBy(),

collectAndThen(maxBy, Optional::get) 

등과 같은 컬렉터들을 상황에 맞게 사용하여 분류 할 수 있습니다.

 

예제코드로 살펴보시죠.

/**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류 함수, counting())
     */
    @DisplayName("각 그룹 별 요리의 갯수 counting")
    @Test
    public void countOfGroupByDish() {
        // when
        Map<Type, Long> groupByAndCountingDish =
                menu.stream()
                        .collect(
                                groupingBy(Dish::getType,
                                        counting()));

        // then
        System.out.println(groupByAndCountingDish); // {MEAT=3, OTHER=4, FISH=2}
    }

    /**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류 함수, summarizingInt())
     */
    @DisplayName("분류한 요리들의 각 summary -번외")
    @Test
    public void summaryOfGroupByDish() {
        // when
        Map<Type, IntSummaryStatistics> summaryOfGroupByDish =
                menu.stream()
                        .collect(
                                groupingBy(Dish::getType,
                                        summarizingInt(Dish::getCalories)));

        // then
        System.out.println(summaryOfGroupByDish);
        /* 결과 값
         * {FISH=IntSummaryStatistics{count=2, sum=750, min=300, average=375.000000, max=450},
         * MEAT=IntSummaryStatistics{count=3, sum=1900, min=400, average=633.333333, max=800},
         * OTHER=IntSummaryStatistics{count=4, sum=1550, min=120, average=387.500000, max=550}}
         */
    }

    /**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류 함수, maxBy())
     */
    @DisplayName("분류된 요리 중 최대 칼로리 값을 가진 요리를 구하기")
    @Test
    public void maxCalOfGroupByDish() {
        // when
        Map<Type, Optional<Dish>> maxCalOfGroupByDish =
                menu.stream()
                        .collect(
                                groupingBy(Dish::getType,
                                        maxBy(Comparator.comparing(Dish::getCalories))));
        // then
        System.out.println(maxCalOfGroupByDish);
        /* 결과
         * {
         * FISH=Optional[salmon],
         * MEAT=Optional[pork],
         * OTHER=Optional[pizza]
         * }
         */
    }

    /**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류함수, collectAndThen(maxBy(), Optional::get)
     */
    @DisplayName("분류된 요리 중 최대 칼로리 값을 가진 요리를 구하기 - collectAndThen 버전")
    @Test
    public void maxCalOfGroupByDishUsingCollectAndThen() {
        // when
        Map<Type, Dish> maxCalOfGroupByDish =
                menu.stream()
                        .collect(
                                groupingBy(Dish::getType,
                                        collectingAndThen(
                                                maxBy(Comparator.comparing(Dish::getCalories)), Optional::get)));

        // then
        System.out.println(maxCalOfGroupByDish);
        /* 결과 값
         * {OTHER=pizza, MEAT=pork, FISH=salmon}
         */
    }


    /**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류 함수, summingInt())
     * - groupingBy와 함께 사용하는 다른 컬렉터 예제
     */
    @DisplayName("분류한 요리들의 각 합계를 구하기.")
    @Test
    public void sumOfGroupByDish() {
        // when
        Map<Type, Integer> sumOfGroupByDish =
                menu.stream()
                        .collect(
                                groupingBy(Dish::getType,
                                        summingInt(Dish::getCalories)));

        // then
        System.out.println(sumOfGroupByDish);
        /* 결과 값
         * {MEAT=1900, OTHER=1550, FISH=750}
         */
    }

    /**
     * 6.3.3 서브그룹으로 데이터 수집 - groupBy(분류 함수, mapping())
     * - groupingBy와 함께 사용하는 다른 컬렉터 예제
     */
    @DisplayName("분류한 요리들을 매핑하기")
    @Test
    public void mappingOfGroupByDish() {
        // when
        Map<Type, Set<CaloricLevel>> calLevelOfGroupByAndMappingDish =
                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))));

        // then
        System.out.println(calLevelOfGroupByAndMappingDish);
        /* 결과 값
         * {FISH=[NORMAL, DIET], OTHER=[NORMAL, DIET], MEAT=[FAT, NORMAL, DIET]}
         */
    }
}

 

6장은 내용이 길어 1편은 이 정도 까지만 정리하고 2편에서 뵙겠습니다.

 

 

참고

모던 자바 인 액션