SummaryBook/[모던 자바 인 액션]

[Chap 6] 스트림으로 데이터 수집

seung_soos 2023. 7. 24. 21:01

6.1 컬렉터란 무엇인가?

Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.

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

collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의 할 수 있다는 점이 컬렉터의 최대 강점이다. 구체적으로 설명해서 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다.

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

Ex) 가장 많이 사용하는 직관적인 정적 메서드로 collect(Collectors.toList);

6.1.2 미리 정의된 컬렉터

미리 정의된 컬렉터는 groupingBy 같이 Collectors 클래스에서 제공하는 팩토리 기능을 설명한다. Collectors에서 제공하는 메서드의 기능은 크게 세가지로 구분 할 수 있다.

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

6.2 리듀싱과 요약

        // Counting 메서드를 이용하여 메뉴에서 요리수를 계산
        long counting1 = menu.stream().collect(Collectors.counting());
        // 불팔요과정을 생략사용가능
        long counting2 = menu.stream().count();

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

       /**
         * 6.2.1 스트림값에서 최댓값과 최솟값 검색
         */
        Comparator<Dish> dishComparator = Comparator.comparingInt(Dish::getCalories);

        Optional<Dish> max = menu.stream().collect(Collectors.maxBy(dishComparator));
        Optional<Dish> min = menu.stream().collect(Collectors.minBy(dishComparator));

6.2.2 요약 연산

Collectors 클래스는 Collectors.summingInt 라는 특별한 요약 팩토리 메서드를 제공한다.

        /**
         * 6.2.2 요약 연산
         */
	// 총 칼로리 계산
        int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

메뉴의 칼로리 총 칼로리를 계산하는 코드이다. summingInt 외에 summingDouble, summingLong 메서드도 있다.

이러한 단순 합계 외에  평균값 계산 등의 연산도 요약 기능으로 제공된다.

        // 칼로리의 평균값 계산
        Double avgCalories = menu.stream().collect(Collectors.averagingInt(Dish::getCalories));

averaginInt, averagingLong, averaginDouble 등 다양한 형식으로 이루어진 숫자 집합의 평균을 계산 할 수 있다.

하나의 요약 연산으로 메뉴에 있는 수, 칼로리합계, 평균, 최댓값, 최솟값등을 계산하는 코드이다.

	menu.add(new Dish("생선", false, 1200, Type.FISH));
        menu.add(new Dish("돼지고기", false, 2300, Type.MEAT));
        menu.add(new Dish("김치", true, 200, Type.OTHER));
        menu.add(new Dish("양파", true, 300, Type.OTHER));
        menu.add(new Dish("소고기", false, 1000, Type.MEAT));

        IntSummaryStatistics collect = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
        System.out.println(collect);

해당결과이다. 마찬가지로 int 뿐 아니라 long이나 double에 대응하는 summarizingLong, summarizingDouble메서드도 있다.

6.2.3 문자열 연결

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

        /**
         * 6.2.3 문자열 연결
         */
        String joiningNames = menu.stream().map(Dish::getName).collect(Collectors.joining());
        System.out.println(joiningNames);

joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.

        // 구분자 사용
        String joiningNamesSeparator = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
        System.out.println(joiningNamesSeparator);

6.2.4 범용 리듀싱 요약 연산

        /**
         * 6.2.4 범용 리듀싱 요약 연산
         */
        Integer reducing1 = menu.stream()
                .collect(Collectors.reducing(0, Dish::getCalories, (a, b) -> a + b));

reducing은 인수 세개를 받는다.

  • 첫 번째 인수는 리듀싱 연산의 시작 초기값이거나, 스트림에 인수가 없을때는 반환값이다.
  • 두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수이다.
  • 세 번째 인수는 같은 종류의 두항목을 하나의 값으로 더하는 BinaryOperator이다.
        // 가장 칼로리가 높은 요리를 찾는 방법
        Optional<Dish> reducing2 = menu.stream()
                .collect(Collectors.reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));

 

한개의 인수를 갖는 reduce

        int totalCalories2 = menu.stream()
        		.map(Dish::getCalories).reduce(Integer::sum).get();

한개의 인수를 갖는 reduce를 스트림에 적용한 다른 예제와 마찬가지로 빈스트림과 관련한 Null 문제를 피하도록 Optional<Integer>를 반환한다.

 

스트림 인터페이스를 잘 사용하면 코드의 재사용성과 커스터 마이즈 가능성이 높아지고, 추상화와 일반화를 얻을 수 있다.

6.3 그룹화

자바 8의 함수형을 이용하면 가독성 있는 한줄의 코드로 그룹화를 구현 할 수 있다.

        /**
         * 6.3 그룹화
         */
        Map<Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
        System.out.println(dishesByType);
        
        menu.stream().collect(Collectors.groupingBy(dish ->{
            if(dish.getCalories() <= 400) return CaloricLevel.DIET;
            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
            else return CaloricLevel.FAT
        }));

각 Key는 Dish의 Type으로 Value는 해당 값이 들어간다.

6.3.1 그룹화된 요소 조작

요소를 그룹화 한 다음에는 각 결과 그룹의 요소를 조작하는 연산

        /**
         * 6.3.1 그룹화된 요소 조작
         */
        // 500 칼로리 이상 필터 후 그룹핑
        Map<Integer, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalories() >= 500)
                .collect(Collectors.groupingBy(Dish::getCalories));

위 코드의 단점으로 메뉴 요리는 맵 형태로 되어있는데, 해당 필터 타입에 맞지않는다면 키가 사라진다.

다음과 같이 수정 할 수 있다.

        Map<Integer, List<Dish>> caloricDishesByType2 = menu.stream().collect(Collectors.groupingBy(Dish::getCalories,
                Collectors.filtering(dish -> dish.getCalories() > 500, Collectors.toList())));
                
        System.out.println("caloricDishesByType1 = " + caloricDishesByType1);
        System.out.println("caloricDishesByType2 = " + caloricDishesByType2);

6.3.2 다수준 그룹화

        /**
         * 6.3.2 다수준 그룹화
         */
        Map<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;
                        })));
        System.out.println("dishesByTypeCaloricLevel = " + dishesByTypeCaloricLevel);

dishesByTypeCaloricLevel = {FISH={DIET=[Dish(name=prawns, vegetarian=false, calories=300, type=FISH)]}, OTHER={NORMAL=[Dish(name=french fries, vegetarian=true, calories=480, type=OTHER)], DIET=[Dish(name=seasonal fruit, vegetarian=true, calories=120, type=OTHER), Dish(name=rice, vegetarian=true, calories=350, type=OTHER)]}, MEAT={DIET=[Dish(name=chicken, vegetarian=false, calories=400, type=MEAT)]}}

 

외부 맵은 첫번째 수준의 분류함수에서 분류한 키값 FISH, MEAT, OTHER로 나누고, 두번째 수준의 분류함수의 NORMAL, DIET, FAT을 키값으로 갖는다.

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

groupingBy 컬렉터를 이용한 count

        /**
         * 6.3.3 서브그룹으로 데이터 수집
         */
        Map<Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType, counting()));
        System.out.println("typesCount = " + typesCount);

typesCount = {FISH=1, OTHER=3, MEAT=1}

요리의 종류 별 칼로리가 가장 높은 요리 출력

        Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(
                groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
        System.out.println("mostCaloricByType = " + mostCaloricByType);

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

마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을  삭제 할 수 있다.

        // Optional 삭제
        Map<Type, Dish> mostCaloricByTypeNotOptional = menu.stream().collect(groupingBy(Dish::getType,
                collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
                        Optional::get)));
        System.out.println("mostCaloricByTypeNotOptional = " + mostCaloricByTypeNotOptional);

팩토리 메서드 collectionAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.

6.4 분할