ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Part2] Java8 In action - Chapter6 - 1
    Java8 In Action 2022. 8. 1. 17:43
    반응형

    해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.

    좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!

    6.1 컬렉터란 무엇인가

    Entity

    Dish.java

    package Part2.Chapter6.Chapter6_6_1.entity;
    
    public class Dish {
        private final String name;
        private final boolean vegetarian;
        private final int calories;
        private Type type;
    
        public Dish(String name, boolean vegetarian, int calories, Type type) {    
            this.name = name;
            this.vegetarian = vegetarian;
            this.calories = calories;
            this.type = type;
        }
    
        public String getName() {
            return this.name;
        }
    
        public int getCalories() {
            return this.calories;
        }
    
        public Type getType() {
            return this.type;
        }
    
        public boolean isVegetarian() {
            return vegetarian;
        }
    
        @Override
        public String toString() {
            return this.name;
        }
    
        public enum Type {
            MEAT, FISH, OTHER
        }
    }

    Main

    package Part2.Chapter6.Chapter6_6_1;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.stream.Collectors;
    
    import Part2.Chapter6.Chapter6_6_1.entity.Dish;
    
    /*
     * 6.1 컬렉터란 무엇인가?
     */
    public class Main_6_1 {
    
        public static void main(String[] args) {
            List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT)    
                , new Dish("beef", false, 700, Dish.Type.MEAT)                
                , new Dish("chicken", false, 400, Dish.Type.MEAT)
                , new Dish("french fries", true, 530, Dish.Type.OTHER)
                , new Dish("rice", true, 350, Dish.Type.OTHER)
                , new Dish("season fruit", true, 120, Dish.Type.OTHER)
                , new Dish("pizza", true, 550, Dish.Type.OTHER)
                , new Dish("prawns", false, 300, Dish.Type.FISH)
                , new Dish("salmon", false, 450, Dish.Type.FISH));
    
            /*
             * 6.1.1 고급 리듀싱 기능을 수행하는 컬렉터
             * 
             *  훌륭하게 설계된 함수형 API의 또 다른 장점은 높은 수준의 조합성과 재사용성을 꼽을 수 있다.
             *  collect로 결과를 수집하는 과정을 간단화하고 유영한 방식으로 정의할 수 있다.
             *  스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.
             *  
             *  보통 함수를 요소로 변환 할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다.
             *  (toList 처럼 데이터 자체를 변환하는 것보다는 데이터 저장 구조를 변환할 때가 많다.)
             *  
             *  아래 코드는 Dish.Type 기준으로 음식명을 그룹화 한 코드이다.
             */
            Map<Dish.Type, List<Dish>> dishResult = new HashMap<>();
    
            for(Dish dish : menu) {
                Dish.Type type = dish.getType();
    
                List<Dish> dishType = dishResult.get(type);
    
                if(dishType == null) {
                    dishType = new ArrayList<>();
                    dishResult.put(type, dishType);
                }
    
                dishType.add(dish);
            }
    
            System.out.println("명령형 버전 : ");
            System.out.println(dishResult);
    
            System.out.println("스트림 버전 : ");
            System.out.println(menu.stream().collect(Collectors.groupingBy(Dish::getType)));
    
            /*
             * 6.1.2 미리 정의된 컬렉터
             * 
             * groupingBy 같이 Collectors 클래스에서 제공하는 팩토리 메서드의 기능을 설명한다.
             * Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다. 
             *         - 스트림 요소를 하나의 값으로 리듀스하고 요약
             *         - 요소 그룹화
             *         - 요소 분할          
             */
    
        }
    
    }

    6.2 리듀싱과 요약

    Entity

    Dish.java

    package Part2.Chapter6.Chapter6_6_2.entity;
    
    public class Dish {
        private final String name;
        private final boolean vegetarian;
        private final int calories;
        private Type type;
    
        public Dish(String name, boolean vegetarian, int calories, Type type) {    
            this.name = name;
            this.vegetarian = vegetarian;
            this.calories = calories;
            this.type = type;
        }
    
        public String getName() {
            return this.name;
        }
    
        public int getCalories() {
            return this.calories;
        }
    
        public Type getType() {
            return this.type;
        }
    
        public boolean isVegetarian() {
            return vegetarian;
        }
    
        @Override
        public String toString() {
            return this.name;
        }
    
        public enum Type {
            MEAT, FISH, OTHER
        }
    }

    Main

    package Part2.Chapter6.Chapter6_6_2;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.IntSummaryStatistics;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.Collector;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    import Part2.Chapter6.Chapter6_6_2.entity.Dish;
    
    /*
     * 6.2 리듀싱과 요약
     * 
     * 컬렉터(Stream.collect 메서드의 인수)로 스트림의 항목을 컬렉션으로 재구성할 수 있다.
     * 좀 더 일반적으로 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠수 있다.
     * 트리를 구성하는 다수준 맵, 메뉴의 칼로리 합계를 가리키는 단순한 정수 등 다양한 형식으로
     * 결과를 도출될 수 있다.
     */
    public class Main_6_2 {
    
        public static void main(String[] args) {
            List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT)    
                , new Dish("beef", false, 700, Dish.Type.MEAT)                
                , new Dish("chicken", false, 400, Dish.Type.MEAT)
                , new Dish("french fries", true, 530, Dish.Type.OTHER)
                , new Dish("rice", true, 350, Dish.Type.OTHER)
                , new Dish("season fruit", true, 120, Dish.Type.OTHER)
                , new Dish("pizza", true, 550, Dish.Type.OTHER)
                , new Dish("prawns", false, 300, Dish.Type.FISH)
                , new Dish("salmon", false, 450, Dish.Type.FISH));
    
            /*
             * 첫 번째 예제로 counting() 이라는 팩토리 메서드가 반환하는 컬렉터로 메뉴에서 요리 수를 계산한다.
             * counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.
             */
            System.out.println("6.2 리듀싱과 요약 - Collectors.counting 사용 : " + menu.stream().collect(Collectors.counting()) );
    
            /*
             * 아래처럼 불필요한 과정을 생략할 수 있다.
             */
            System.out.println("6.2 리듀싱과 요약 - count 메서드 사용 : " + menu.stream().count() );
    
            /*
             * 6.2.1 스트림값에서 최대값과 최소값 검색
             * 
             * 메뉴에서 칼로리가 가장 높거나 낮은 요리를 찾는다고 가정하자. 
             * Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용할 수 있다.
             * 두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받는다.
             * 
             * Optional<Dish>는 만약 menu가 비어있다면 그 어떤 요리도 반환되지 않을 것이다.
             * 자바 8은 값을 포함하거나 포함하지 않을 수 있는 컨테이너 Optional을 제공한다.
             * 
             * 또한 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이
             * 자주 사용된다. 이러한 연산을 요약(summarization)연산이라 부른다.
             */
            Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
    
            Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));
            System.out.print("6.2.1 스트림값에서 최대값과 최소값 검색- Collectors.maxBy 사용 : ");
            System.out.println("이름 = " + mostCalorieDish.get() + ", 칼로리 = " + mostCalorieDish.get().getCalories() );
    
            mostCalorieDish = menu.stream().collect(Collectors.minBy(dishCaloriesComparator));
            System.out.print("6.2.1 스트림값에서 최대값과 최소값 검색 - Collectors.minBy 사용 : ");
            System.out.println("이름 = " + mostCalorieDish.get() + ", 칼로리 = " + mostCalorieDish.get().getCalories() );
    
            /*
             * 6.2.2 요약 연산
             * 
             * Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다.
             * summingInt는 객체를 int로 매핑하는 함수를 인수로 받고 인수로 전달된 함수는 객체를 int로
             * 매핑한 컬렉터를 반환한다.
             * 그리고 summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다.
             * 
             * 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드이다.
             */
            System.out.println("6.2.2 요약 연산 - Collectors.summingInt : " 
                + menu.stream().collect(Collectors.summingInt(Dish::getCalories)) );    
    
            /*
             * Collectors.summingLong과 Collectors.summingDouble도 summingInt와 같은 방식으로 동작하며
             * 각각 long 또는 double 형식의 데이터로 요약한다는 점만 다르다.
             * 
             * 이러한 단순 합계 외 평균값 계산 등의 연산도 요약 기능으로 제공된다.
             * 즉, Collectors.averagingInt, Collectors.averagingLong, Collectors.averagingDouble 등
             * 다양한 숫자 집합의 평균을 계산할 수 있다.
             */
            System.out.println("6.2.2 요약 연산 - Collectors.averagingInt : " 
                + menu.stream().collect(Collectors.averagingInt(Dish::getCalories)) );
    
            /*
             * 간혹 위 연산 중 두 개 이상의 연산을 한번에 수행해야 할 때도 있다.
             * 이런 상황에서는 팩토리 메서드 summarizingInt가 반환하는 컬렉터를 사용할 수 있다. 
             * 
             * int뿐 아니라 long이나 double에 대응하는 Collectors.summarizingLong, Collectors.summarizingDouble 메서드와
             * 관련된 LongSummaryStatistics, DobuleSummaryStatistics 클래스도 있다.
             */
            IntSummaryStatistics menuStatistics = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
            System.out.println("6.2.2 요약 연산 - Collectors.summarizingInt : " + menuStatistics);
    
            /*
             * 6.2.3 문자열 연결
             * 
             * 컬렉터에서 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서
             * 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
             * 
             * 아래 코드는 메뉴의 모든 요리명을 연결하는 코드다.
             */
            System.out.println("6.2.3 문자열 연결 - joining : " 
                + menu.stream().map(Dish::getName).collect(Collectors.joining()));
    
            /*
             * joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 만든다.
             * 연결된 두 요소 사이에 구분 문자열을 넣을 수 있도록 오버로드된 joining 팩토리 메서드도 있다.
             */
            System.out.println("6.2.3 문자열 연결 - joining : " 
                + menu.stream().map(Dish::getName).collect(Collectors.joining(", ")));
    
            /*
             * 6.2.4 범용 리듀싱 요약 연산
             * 
             * 모든 컬렉터는 reducing 팩토리 메서드도 정의할 수 있다.
             * 즉, 범용 Collectors.reducing으로도 구현할 수 있다.
             * 이전까지 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다.
             * 
             * 아래 코드는 reducing 메서드로 메뉴의 모든 칼로리 합계이다.
             */
            System.out.println("6.2.4 범용 리듀싱 요약 연산 - 모든 칼로리 합계 : " 
                + menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j) ));
    
            /*
             * reducing은 세 개의 인수를 받는다.
             *         - 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때 반환값.
             *         (숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다.)
             *         - 두 번째 인수는 요리를 칼로리 정수로 변환할 때 사용한 변환 함수이다.
             *         - 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.
             * 
             * 다음 코드 처럼 한 개의 인수를 가진 reducing을 이용해서 칼로리가 가장 높은 요리를 찾는 방법이다.
             */
            System.out.println("6.2.4 범용 리듀싱 요약 연산 - 칼로리가 가장 높은 요리 : " 
                + menu.stream().collect(Collectors.reducing((d1, d2) 
                    -> d1.getCalories() > d2.getCalories()? d1 : d2)).get() );
    
            /*
             * 한 개의 인수를 갖는 reducing 팩터리 메서드는 세 개의 인수를 갖는 reducing 메서드에서
             * 스트림의 첫 번째 요소를 시작 요소, 즉 첫 번째 인수로 받으며 자신을 그대로 반환하는 
             * 항등함수(identity function)를 두 번째 인수로 받는 상황에 해당한다.
             * 
             * 즉, 한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌을 때
             * 시작값이 설정되지 않는 상황이 생기므로 Optional<Dish> 객체를 반환한다.
             */
    
            /*
             * collect와 reduce
             * 
             * 위 코드는 의미론적인 문제와 실용적인 문제 등 두 가지 문제가 발생한다.
             * collect 메서드는 도출 하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드인 반면
             * reduce는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 일어난다.
             * 아래 코드에 reduce메서드는 누적자로 사용된 리스트를 변환시키므로 reduce를 잘못 활용한 예이다.
             * 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가지므로 리듀싱 연산을 병렬로
             * 수행할 수 없다는 점도 문제다.
             * 이 문제를 해결하려면 매번 새로운 리스트를 할당해야 하고 따라서 객체를 할당하느라 성능이 저하 될 것이다.
             * 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직 한다.
             */
            Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6).stream();
            System.out.println(stream.reduce(new ArrayList<Integer>()
                , (List<Integer> l, Integer e) -> {
                    l.add(e);
                    return l;
                }
                ,(List<Integer> l1, List<Integer> l2) -> {
                    l1.addAll(l2);
                    return l1;
                }));
    
            /*
             * 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.!
             * '6.2.4 범용 리듀싱 요약 연산 - 모든 칼로리 합계' 예제에서 람다 표현식 대신
             * Integer클래스의 sum 메서드 레퍼런스를 이용하면 코드를 좀 더 단순화 할 수 있다.
             */
            System.out.println("모든 칼로리 합계(sum 메서드 레퍼런스) : " + menu.stream()
                .collect(Collectors.reducing(0, Dish::getCalories, Integer::sum))); 
    
            System.out.println(menu.stream()
                .collect(
                    /*
                     * counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서
                     * 구현할 수 있다.
                     */
                    counting()
                ).longValue() 
            );
    
            /*
             * 자신의 상황에 맞는 최적의 해법 선택
             * 
             * 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것보다 컬렉터를 이용하는 코드가
             * 더 복잡하다는 사실도 보여준다.
             * 코드가 좀 더 복잡한 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와
             * 일반화를 얻을 수 있다.
             * 
             * 문제를 해결할 수 있는 다양한 해결 방법을 확인한 다음에 가장 일반적으로 문제에 특화된 해결책을
             * 고르는 것이 바람직하다. 
             */
        }
    
        public static <T> Collector<T, ?, Long> counting() {
            /*
             * 스트림의 Long 객체 형식의 요소를 1로 변환한 다음에 모두 더할 수 있다.
             */
            return Collectors.reducing(0L, e -> 1L, Long::sum);
        }
    }

    6.3 그룹화

    Entity

    Dish.java

    package Part2.Chapter6.Chapter6_6_3.entity;
    
    public class Dish {
        private final String name;
        private final boolean vegetarian;
        private final int calories;
        private Type type;
    
        public Dish(String name, boolean vegetarian, int calories, Type type) {    
            this.name = name;
            this.vegetarian = vegetarian;
            this.calories = calories;
            this.type = type;
        }
    
        public String getName() {
            return this.name;
        }
    
        public int getCalories() {
            return this.calories;
        }
    
        public Type getType() {
            return this.type;
        }
    
        public boolean isVegetarian() {
            return vegetarian;
        }
    
        @Override
        public String toString() {
            return this.name;
        }
    
        public enum Type {
            MEAT, FISH, OTHER
        }
    }

    Main

    package Part2.Chapter6.Chapter6_6_3;
    
    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Optional;
    import java.util.stream.Collectors;
    
    import Part2.Chapter6.Chapter6_6_3.entity.Dish;
    
    /*
     * 6.3 그룹화
     * 
     * 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다. 
     * 자바 8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.
     */
    public class Main_6_3 {
    
        public enum CaloricLevel {DIET, NORMAL, FAT}
    
        public static void main(String[] args) {
            List<Dish> menu = Arrays.asList(
                new Dish("pork", false, 800, Dish.Type.MEAT)    
                , new Dish("beef", false, 700, Dish.Type.MEAT)                
                , new Dish("chicken", false, 400, Dish.Type.MEAT)
                , new Dish("french fries", true, 530, Dish.Type.OTHER)
                , new Dish("rice", true, 350, Dish.Type.OTHER)
                , new Dish("season fruit", true, 120, Dish.Type.OTHER)
                , new Dish("pizza", true, 550, Dish.Type.OTHER)
                , new Dish("prawns", false, 300, Dish.Type.FISH)
                , new Dish("salmon", false, 450, Dish.Type.FISH));
    
            /*
             * 아래 코드는 고기를 포함한 그룹, 생성을 포함한 그룹, 나머지 그룹으로 메뉴화한 코드이다.
             * 
             * 스트림의 각 요리에서 Dish.Type과 일치하는 모든 요리를 추출하는 함수를 groupingBy 메서드로 전달했다.
             * 이 함수를 기준으로 스트림이 그룹화하므로 이를 "분류 함수(classification function)"라고 부른다. 
             */
            System.out.println(menu.stream()
                .collect(Collectors.groupingBy(Dish::getType)) );
    
            /*
             * 아래 코드 같이 400칼로리 이하를 'diet'로 400 ~ 700칼로리를 'normal'로 700 칼로리 초과를 'fat' 요리로
             * 분류한다고 가정하면 Dish 클래스에는 이러한 연산에 필요한 메서드가 없으므로 메서드 레퍼런스를 분류 함수로
             * 사용할 수 없다. 따라서 메서드 레퍼런스 대신 람다 표현식으로 필요한 로직을 구현할 수 있다.
             */
            System.out.println(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;
                    }                
                })) );
    
            /*
             * 6.3.1 다수준 그룹화
             * 
             * 두 인수를 받는 팩토리 메서드 Collectors.groupingBy를 이용해서 항목을 다수준으로 그룹화 할 수 있다.
             * Collectors.groupingBy는 일반적인 분류 함수와 컬렉터를 인수로 받는다.
             * 즉, 바깥 쪽 groupingBy 메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 groupingBy를
             * 전달해서 두 수준으로 스트림의 항목을 그룹화할 수 있다.
             * 
             * 보통 groupingBy의 연산을 버킷(bucket - 물건을 담을 수 있는 양동이) 개념으로 생각하면 쉽다.
             * 첫 번째 groupingBy는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서버스트림 컬렉터로
             * 채워가기를 반복하면서 n수준 그룹화를 달성한다.
             */
            System.out.println(menu.stream()
                .collect(Collectors.groupingBy(
                    /*
                     * 외부 맵은 첫 번째 수준의 분류 함수에서 분류한 키값(fish, meat, other)를 갖는다.
                     */
                    Dish::getType
                    /*
                     * 그리고 외부 맵의 값은 두 번째 수준의 분류 함수의 기준 'normal', 'diet', 'fat'을 키값을 갖는다.
                     * 최종적으로 두 수준의 맵은 첫 번째 키와 두 번째 키의 수준에 부합하는 요소 리스트를 값(salmon, pizza)으로 갖는다.
                     * 다수준 그릅화 연산은 다양한 수준으로 확장할 수 있따.
                     * 즉, n수준 그룹화의 결과는 n수준 트리 구조로 표현되는 n수준 맵이 된다.
                     */
                    , Collectors.groupingBy(dish -> {
                        if(dish.getCalories() <= 400) {
                            return CaloricLevel.DIET;
                        } else if(dish.getCalories() <= 700) {
                            return CaloricLevel.NORMAL;
                        } else {
                            return CaloricLevel.FAT;
                        }                
                    })
                )
            ));
    
            /*
             * 6.3.2 서브그룹으로 데이터 수집
             * 
             * 첫 번쨰 groupingBy로 넘겨주는 컬렉터의 형식은 제한이 없다.
             * 예를 들어 다음 코드처럼 groupingBy 컬렉터에 두 번째 인수로 counting 컬렉터를 전달해서
             * 메뉴에서 요리의 수를 종류별로 계산할 수 있다.
             * 
             * 분류 함수 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 축약형이다.
             */
            System.out.println("6.3.2 서브그룹으로 데이터 수집 : " + menu.stream()
                .collect(Collectors.groupingBy(Dish::getType, Collectors.counting())) );
    
            /*
             * 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 아래와 같이 재구현할 수 있다.
             */
            System.out.println("6.3.2 서브그룹으로 데이터 수집 : " + menu.stream()
                    .collect(Collectors.groupingBy(Dish::getType
                        , Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))) ));
    
            /*
             * 컬렉터 결과를 다른 형식에 적용하기
             * 
             * 위 코드에서 마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없기 때문에
             * Optional을 삭제 할 수 있다.
             * 즉, 다음 처럼 팩토리 메서드 Collectors.collectingAndThen으로 컬렉터가 반환한 결과를
             * 다른 형식으로 활용할 수 있다.
             * 
             * 펙토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
             * 반환되는 컬렉터는 기존 컬렉터의 래퍼 역할을 하며 collect의 마지막 과정에서 변환 함수로 자신이 반환하는
             * 값을 매핑한다.
             * 
             * 아래 예제는 maxBy로 만들어진 컬렉터가 감싸지는 컬렉터며 변환 함수 Optional::get으로 반환된 Optional에
             * 포함된 값을 추출한다.
             * 
             * 이미 언급했듯이 리듀싱 컬렉터는 Optional.empty()를 반환하지 않으므로 안전한 코드이다.
             */
            System.out.println("6.3.2 서브그룹으로 데이터 수집 : " + menu.stream()
                .collect(Collectors.groupingBy(
                    /*
                     * 요리의 종류에 따라 메뉴 스트림을 서브스트림으로 그룹화 한다.
                     */
                    Dish::getType
                    /*
                     * Collectors.collectingAndThen 컬렉터는 세 번째 컬렉터 maxBy를 감싼다.
                     */
                    , Collectors.collectingAndThen(
                        Collectors.maxBy(Comparator.comparingInt(Dish::getCalories))
                        /*
                         * 리듀싱 컬렉터가 서브스트림에 연산을수행한 결과에 collectingAndThen의 Optional::get
                         * 변환 함수가 적용된다.
                         */                    
                        , Optional::get
                    )
                )
            ) );
    
            /*
             * groupingBy와 함께 사용하는 다른 컬렉터 예제
             * 
             * 일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 때는
             * 팩토리 메서드 groupingBy에 두 번째 인수로 전달한 컬렉터를 사용한다.
             * 
             * 예를 들어 메뉴에 있는 모든 요리의 칼로리 합계를 구하려고 만든 컬렉터를 재사용할 수 있다.
             */
            System.out.println("groupingBy와 함께 사용하는 다른 컬렉터 예제 : " + menu.stream()
                .collect(Collectors.groupingBy(
                    Dish::getType
                    , Collectors.summingInt(Dish::getCalories) )
                ) );
    
            /*
             * 이 외에도 mapping 메서드로 만들어진 컬렉터도 groupingBy와 자주 사용된다.
             * mapping 메서드는 스트림의 인수를 변환하는 함수와 변환 함수의 결과 객체를 누적하는 컬렉터를
             * 인수로 받는다.
             * mapping은 입력 요소를 누적하기 전에 매핑 함수를 적용해서 다양한 형식의 객체를 주어진 형식의
             * 컬렉터에 맞게 변환하는 역할을 한다.
             * 다음 코드처럼 groupingBy와 mapping 컬렉터를 합친 기능이다.
             */
            System.out.println("groupingBy와 함께 사용하는 다른 컬렉터 예제 : " + menu.stream()
                .collect(Collectors.groupingBy(
                    Dish::getType
                    , Collectors.mapping(
                    // 스트림의 인수를 변환하는 함수
                    dish -> {
                        if(dish.getCalories() <= 400) {
                            return CaloricLevel.DIET;
                        } else if(dish.getCalories() <= 700) {
                            return CaloricLevel.NORMAL;
                        } else {
                            return CaloricLevel.FAT;
                        }
                    }
                    // 변환 함수의 결과 객체를 누적하는 컬렉터.
                    , Collectors.toCollection(HashSet::new) )
                    /*, Collectors.toSet() )*/
                )
            ) );
        }
    
    }

    요약

    • collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방법(컬렉터라 불리는)을 인수로 갖는 최종 연산이다.
    • 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최소값, 최대값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있다.
    • 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할할 수 있다.
    • 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.
    • Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다.
    반응형

    댓글

Designed by Tistory.