ABOUT ME

-

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

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

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

    6.4 분할

    Entity

    Dish.java

    package Part2.Chapter6.Chapter6_6_4.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_4;
    
    import java.util.Arrays;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Map;
    import java.util.Optional;
    import java.util.function.Predicate;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    import Part2.Chapter6.Chapter6_6_4.entity.Dish;
    
    /*
     * 6.4 분할
     * 
     * 분할은 "분할 함수(partitioning function)"라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.
     * 분할 함수는 불린을 반환하므로 맵의 키 형식은 Boolean이다.
     * 결과적으로 그룹화 맵은 최대(참 아니면 거짓의 값을 갖는) 두 개의 그룹으로 분류 된다.
     */
    public class Main_6_4 {
    
        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));
    
            /*
             * 아래 코드는 모든 요리를 채식 요리와 채식이 아닌 요리로 분류한 코드이다.
             */
            Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
                .collect(Collectors.partitioningBy(Dish::isVegetarian));
    
            System.out.println("6.4 분할 : " +partitionedMenu);        
            System.out.println("6.4 분할(채식 요리): " + partitionedMenu.get(true));
            System.out.println("6.4 분할(채식이 아닌 요리): " + partitionedMenu.get(false));
    
            Predicate<Dish> isVegetarian = Dish::isVegetarian;
    
            System.out.println("6.4 분할(filter 메서드 사용 - 채식 요리) : " + menu.stream()
                .filter(isVegetarian)
                .collect(Collectors.toList()) );
    
            System.out.println("6.4 분할(filter 메서드 사용 - 채식이 아닌 요리) : " + menu.stream()
                .filter(isVegetarian.negate())
                .collect(Collectors.toList()) );
    
            /*
             * 6.4.1 분할의 장점
             * 
             * 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점이다.
             * 다음 코드에서는 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드된 버전의 partitioningBy 메서드도 있다.
             */
            System.out.println("6.4.1 분할의 장점 : " + menu.stream()
                .collect(Collectors.partitioningBy(Dish::isVegetarian
                    , Collectors.groupingBy(Dish::getType)) ));        
    
            /*
             * 채식 요리와 채식이 아닌 요리 각각 그룹에서 칼로리가 높은 요리.
             */
            System.out.println("6.4.1 분할의 장점 : " + menu.stream()
                .collect(Collectors.partitioningBy(Dish::isVegetarian
                    , Collectors.collectingAndThen(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)) ));
    
            /*
             * 6.4.2 숫자를 소수와 비소수로 분할하기
             * 
             * 정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수(prime)와 비소수(nonprime)로 나누는 코드를 구현하자.
             */
            System.out.println("6.4.2 숫자를 소수와 비소수로 분할하기 : " + IntStream.rangeClosed(2, 100).boxed()
                .collect(Collectors.partitioningBy(candidate -> isPrime(candidate)) ));
    
            /*
             * Collectors 클래스의 정적 팩토리 메서드
             * 
             * 팩토리 메서드 : toList
             * 반환 형식 : List<T>
             * 사용 예제 : 스트림의 모든 항목을 리스트로 수집.
             * 활용 예 : List<Dish> dishes = menuStream.collect(Collectors.toList());
             * 
             * 팩토리 메서드 : toSet
             * 반환 형식 : Set<T>
             * 사용 예제 : 스트림의 모든 항목을 중복이 없는 집합으로 수집.
             * 활용 예 : Set<Dish> dishes = menuStream.collect(Collectors.toSet());
             * 
             * 팩토리 메서드 : toCollection
             * 반환 형식 : Collection<T>
             * 사용 예제 : 스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집.
             * 활용 예 : Collection<Dish> dishes = menuStream.collect(Collectors.toCollection(), ArrayList::new);
             * 
             * 팩토리 메서드 : counting 
             * 반환 형식 : Long
             * 사용 예제 : 스트림의 항목 수 계산.
             * 활용 예 : Long howManyDishes = menuStream.collect(Collectors.counting());
             * 
             * 팩토리 메서드 : summingInt 
             * 반환 형식 : Integer
             * 사용 예제 : 스트림의 항목에서 정수 프로퍼티값을 더함.
             * 활용 예 : int totalCalories = menuStream.collect(Collectors.sumingInt(Dish::getCalories));
             * 
             * 팩토리 메서드 : averagingInt 
             * 반환 형식 : Double
             * 사용 예제 : 스트림 항목의 정수 프로퍼티의 평균값 계산.
             * 활용 예 : double avgCalories = menuStream.collect(Collectors.averagingInt(Dish:getCalories));
             * 
             * 팩토리 메서드 : summarizingInt 
             * 반환 형식 : IntSummaryStatistics
             * 사용 예제 : 스트림 내의 항목의 최대값, 최소값, 합계 평균 등의 정수 정보 통계를 수집.
             * 활용 예 : IntSummaryStatistics menuStatistics = menuStream.collect(Collectors.summarizingInt(Dish::getCalories));
             * 
             * 팩토리 메서드 : joining 
             * 반환 형식 : String
             * 사용 예제 : 스트림의 각 항목에 toString 메서드를 호출한 결과 문자열을 연결.
             * 활용 예 : String shortMenu = menuStream.map(Dish::getName).collect(Collectors.joining(", "));
             * 
             * 팩토리 메서드 : maxBy 
             * 반환 형식 : Optional<T>
             * 사용 예제 : 주어진 비교자를 이용해서 스트림의 최대값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()을 반환.
             * 활용 예 : Optional<Dish> fattest = menuStream.collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
             * 
             * 팩토리 메서드 : minBy 
             * 반환 형식 : Optional<T>
             * 사용 예제 : 주어진 비교자를 이용해서 스트림의 최소값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을 때는 Optional.empty()을 반환.
             * 활용 예 : Optional<Dish> fattest = menuStream.collect(Collectors.minBy(Comparator.comparingInt(Dish::getCalories)));
             * 
             * 팩토리 메서드 : reducing 
             * 반환 형식 : 리듀싱 연산에서 형식을 결정
             * 사용 예제 : 누적자를 초기값으로 설정한 다음에 BinaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱.
             * 활용 예 : int totalCalories = menuStream.collect(Collectors.reducing(0, Dish::getCalories, Integer::sum));
             * 
             * 팩토리 메서드 : collectingAndThen 
             * 반환 형식 : 변환 함수가 형식을 결정
             * 사용 예제 : 다른 컬렉터를 감싸고 그 결과에 변환 함수를 적용
             * 활용 예 : int howManyDishes = menuStream.collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
             * 
             * 팩토리 메서드 : groupingBy 
             * 반환 형식 : Map<K, List<T>>
             * 사용 예제 : 하나의 프로퍼티값을 기준으로 스트림의 항목을 그룹화하며 기준 프로퍼티 값을 결과 맵의 키로 사용.
             * 활용 예 : Map<Dish.Type, List<Dish>> dishesByType = menuStream.collect(Collectors.groupingBy(Dish::getType));
             * 
             * 팩토리 메서드 : partitioningBy 
             * 반환 형식 : Map<Boolean, List<T>>
             * 사용 예제 : 프레디케이트를 스트림의 각 항목에 적용한 결과로 항목을 분할.
             * 활용 예 : Map<Boolean, List<Dish>> vegetarianDishes = menuStream.collect(Collectors.partitioningBy(Dish::isVegetarian));
             */
    
        }
    
        public static boolean isPrime(int candidate) {
            int candidateRoot = (int) Math.sqrt((double) candidate);
    
            return IntStream.rangeClosed(2,  candidateRoot)
                .noneMatch(i -> candidate % i == 0);
        }
    
    }

    6.5 Collector 인터페이스

    Entity

    Dish.java

    package Part2.Chapter6.Chapter6_6_5.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_5;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.EnumSet;
    import java.util.List;
    import java.util.Set;
    import java.util.function.BiConsumer;
    import java.util.function.BinaryOperator;
    import java.util.function.Function;
    import java.util.function.Supplier;
    import java.util.stream.Collector;
    import java.util.stream.Collectors;
    
    import Part2.Chapter6.Chapter6_6_5.entity.Dish;
    
    /*
     * 6.5 Collector 인터페이스
     * 
     * Collector 인터페이스는 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
     * 
     * Collector 인터페이스
     * public interface Collector<T, A, R> {
     *         Supplier<A> supplier();
     *         BiConsumer<A, T> accumulator();
     *         Function<A, R> finisher();
     *         BinarayOperator<A> cambiner();
     *         Set<Characteristics> characteristics();
     * }
     * 
     * 위 코드는 다음처럼 설명할 수 있다.
     *         - T는 수집될 스트림 항목의 제네릭 형식이다.
     *         - A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
     *         - R은 수집 연산 결과 객체의 형식이다.
     *         (항상 그런 것은 아니지만 대개 컬렉터 형식)
     */
    public class Main_6_5 {
    
        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));
    
            System.out.println("Collectors.toList : " + menu.stream().collect(Collectors.toList()) );
            System.out.println("ToListCollector : " + menu.stream().collect(new ToListCollector<Dish>()) );
    
            /*
             * 컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기
             * 
             * IDENTITY_FINISH 수집 연산에서는 Collector 인터페이스를 완전히 새로 구현하지 않고도 같은 결과를 얻을 수 있다.
             * Stream은 세 함수(supplier, accumulator, combiner)를 인수로 받는 collect 메서드를 오버로드하며 각각의 메서드는
             * Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 한다.
             * 
             * 아래 코드가 좀 더 간결하고 축약되어 있지만 가독성은 떨어진다.
             * 적절한 클래스로 커스텀 컬렉터를 구현하는 편이 중복을 피하고 재사용성을 높이는데 도움이 된다. 
             * 또한 Characteristics를 전달할 수 없다. 즉, 두 번째 collect 메서드는 IDENTITY_FINISH와 CONCURRENT지만 
             * UNORDERED는 아닌 컬렉터로만 동작한다.
             */
            System.out.println("인수 사용 : " + menu.stream().collect(ArrayList::new, List::add, List::addAll));
        }
    
    }
    
    /*
     * 6.5.1 Collector 인터페이스의 메서드 살펴보기
     * 
     * 네 개의 메서드는 collect 메서드에서 실행하는 함수를 반환하는 반면, 다섯 번째 메서드 characteristics는
     * collect 메서드가 어떤 최적화(병렬화 같은)를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는
     * 힌트 특성 집합을 제공한다.
     * 
     * 실제로 collect가 동작하기 전에 다른 중간 연산과 파이프라인을 구성할 수 있게 해주는 게으른 특성 그리고 
     * 병렬실행 등도 고려해야 하므로 스트림 리듀싱 기능 구현은 생각보다 복잡하다.
     */
    class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
    
        /*
         * supplier 메서드 : 새로운 결과 컨테이너 만들기
         */
        @Override
        public Supplier<List<T>> supplier() {
            /*
             * supplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 한다.
             * 즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.
             * 
             * 해당 클래스처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
             */
            return () -> new ArrayList<T>();
    
            /*
             * 아래와 같이 하면 더 간결해진다.
             */
            // return ArrayList::new;
        }
    
        /*
         * accumulator 메서드 : 결과 컨테이너에 요소 추가하기 
         */
        @Override
        public BiConsumer<List<T>, T> accumulator() {
            /*
             * accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다.
             * 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.
             * 함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
             * 
             * 해당 클래스에서 accumulator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산을 수행한다.
             */
            return (List<T> list, T item) -> list.add(item);
    
            /*
             * 아래와 같이 하면 더 간결해진다.
             */
            // return List::add; 
        }
    
        /*
         * finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기
         */
        @Override
        public Function<List<T>, List<T>> finisher() {
            /*
             * finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끌낼 때 호출할 함수를 반환해야 한다.
             * 때로는 해당 클래스처럼 누적자 객체가 이미 최종 결과인 상황도 있다.
             * 
             * 이런 때는 변환 과정이 필요하지 않으므로 finisher 메서드는 항등 함수를 반환한다.
             * 
             * supplier, accumulator, finisher의 세 가지 메서드로도 순차적 스트림 리듀싱 기능을 수행할 수 있다. 
             */
            return (list) -> list;
    
            /*
             * 아래와 같이 하면 더 간결해진다.
             */
            //return Function.identity();
        }
    
        /*
         * combiner 메서드 : 두 결과 컨테이너 병합 
         */
        @Override
        public BinaryOperator<List<T>> combiner() {
            /*
             * combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.
             * toList의 combiner는 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트 뒤에 추가하면 되기 때문에
             * 비교적 쉽게 구현할 수 있다.
             */
            return (list1, list2) -> {
                list1.addAll(list2);
    
                return list1;
            };
        }
    
        /*
         * Characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
         */
        @Override
        public Set<Characteristics> characteristics() {
            /*
             * Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
             * 
             * UNORDERED
             * 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
             * 
             * CONCURRENT
             * 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.
             * 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은(집합처럼 요소의 순서에 무의미한)상황에서만 
             * 병렬 리듀싱을 수행할 수 있다.
             * 
             * IDENTITY_FINISH
             * finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다.
             * 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다.
             * 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
             * 
             * 해당 클래스는 스트림의 요소를 누적하는데 사용한 리스트가 최종 결과 형식이므로 추가 변환이 필요없다.
             * 그러므로 IDENTITY_FINISH다. 하지만 리스트의 순서는 상관이 없으므로 UNORDERED다. 그리고 마지막으로 CONCURRENT다.
             * 하지만 이미 설명했듯이 요소의 순서가 무의미한 데이터 소스여야 병렬로 실행할 수 있다.
             */
            return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH, Characteristics.CONCURRENT));
        }
    
    
    }

    6.6 커스텀 컬렉터를 구현해서 성능 개선하기

    Main

    package Part2.Chapter6.Chapter6_6_6;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.EnumSet;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.function.BiConsumer;
    import java.util.function.BinaryOperator;
    import java.util.function.Function;
    import java.util.function.Predicate;
    import java.util.function.Supplier;
    import java.util.stream.Collector;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;
    
    /*
     * 6.6 커스텀 컬렉터를 구현해서 성능 개선하기
     * 
     * 커스텀 컬렉터로 n까지의 자연수를 소수와 비소수 분할 성능 개선.
     */
    
    /*
     * 1단계 : Collector 클래스 시그너처 정의
     * 
     * 정수로 이루어진 스트림에서 누적자와 최종 결과의 형식이 Map<Boolean, List<Integer>>인 컬렉터를 구현한다.
     * 즉, Map<Boolean, List<Integer>>는 참과 거짓을 키로 소수와 비소수를 구분 짓는다.
     */
    class PrimeNumbersCollector implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {
        private <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
            int i = 0;
    
            for(A item : list) {
                // 리스트의 현재 요소가 프레디케이트를 만족하는지 검사한다.
                if(!p.test(item)) {
                    // 프레디케이트를 만족하지 않으면 검사한 항목의 앞쪽에 위치한 서브 리스트를 반환한다.
                    return list.subList(0, i);
                }
    
                i++;
            }
    
            return list;
        }
    
        private boolean isPrime(List<Integer> primes, int candidate) {
            int candidateRoot = (int) Math.sqrt((double) candidate);
    
            return takeWhile(primes, i -> {
                    return i <= candidateRoot;    
                })
                .stream().noneMatch(i -> {
                    return candidate % i == 0;
                });
        }
    
        /*
         * 2단계 : 리듀싱 연산 구현 
         */
        @Override
        public Supplier<Map<Boolean, List<Integer>>> supplier() {
            /*
             * 누적자로 사용할 맵을 만들면서 true, false 키와 빈 리스트로 초기화 한다.
             * 수집 과정에서 빈 리스트에 각각 소수와 비소수를 추가할 것이다.
             */
            return () -> new HashMap<Boolean, List<Integer>>() {
                private static final long serialVersionUID = 1L;
    
                {
                    put(true, new ArrayList<Integer>());
                    put(false, new ArrayList<Integer>());
                }
            };
        }
    
        @Override
        public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
            /*
             * 지금까지 발견한 소수 리스트(누적 맵의 true 키로 이들 값에 접근할 수 있다)와 소수 여부를 확인하고 싶은 candidate를
             * 인수로 isPrime 메서드를 호출 했다.
             * isPrime의 호출 결과로 소수 리스트 또는 비소수 리스트 중 알맞는 리스트로 candidate를 추가한다.
             */
            return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
                acc.get(isPrime(acc.get(true), candidate)).add(candidate);
            };
        }
    
        /*
         * 3단계 : 병렬 실행할 수 있는 컬렉터 만들기(가능하다면)
         * 
         * 예제에서는 단순하게 두 번째 맵의 소수 리스트와 비소스 리스트의 모둔 수를 첫 번째 맵에 추가하는 연산이면 충분하다. 
         */
        @Override
        public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
            /*
             * 참고로 알고리즘 자체가 순차적이어서 컬렉터를 실제 병렬로 사용할 순 없다.
             * 따라서 combiner 메서드는 호출될 일이 없으므로 빈 구현으로 남겨두거나 UnsupportedOperationException을 던지도록 구현한다.
             * 실제로 이 메서드는 사용할 일이 없지만 학습을 목적으로 구현한 것이다.
             */
            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;
            };
        }
    
        /*
         * 4단계 : finisher 메서드와 컬렉터의 characteristics 메서드 
         */
        @Override
        public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
            /*
             * accumulator의 형식은 컬렉터 결과 형식과 같으므로 변환 과정이 필요없다. 따라서 항등 함수 identity를 반환한다.
             */
            return Function.identity();
        }
    
        @Override
        public Set<Characteristics> characteristics() {
            /*
             * 해당 커스텀 컬렉터는 CONCURRENT도 아니고 UNORDERED도 아니지만 IDENTITY_FINISH이므로 아래 처럼 구현할 수 있다.
             */
            return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH) );
        }
    }
    
    public class Main_6_6 {
        public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
            return IntStream.rangeClosed(2, n).boxed()
                .collect(Collectors.partitioningBy(candidate -> {
                    int candidateRoot = (int) Math.sqrt((double) candidate);
    
                    return IntStream.rangeClosed(2, candidateRoot).noneMatch(i -> candidate % i == 0);
                }));
        }
    
        public static Map<Boolean, List<Integer>> partitionPrimesCustomCollector(int n) {
            return IntStream.rangeClosed(2, n).boxed()
                .collect(new PrimeNumbersCollector());
        }
    
        public static <A> List<A> takeWhile(List<A> list, Predicate<A> p) {
            int i = 0;
    
            for(A item : list) {
                // 리스트의 현재 요소가 프레디케이트를 만족하는지 검사한다.
                if(!p.test(item)) {
                    // 프레디케이트를 만족하지 않으면 검사한 항목의 앞쪽에 위치한 서브 리스트를 반환한다.
                    return list.subList(0, i);
                }
    
                i++;
            }
    
            return list;
        }
    
        public static boolean isPrime(List<Integer> primes, int candidate) {
            int candidateRoot = (int) Math.sqrt((double) candidate);
    
            return takeWhile(primes, i -> {
                    return i <= candidateRoot;    
                })
                .stream().noneMatch(i -> {
                    return candidate % i == 0;
                });
        }
    
        public static Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) {
            /*
             * collect 메서드의 오버로드를 이용해서 핵심 로직을 구현하는 세 함수를 전달해서 같은 결과를 얻을 수 있다.
             * 코드는 간결하지만 재사용성은 떨어진다.
             */        
            return IntStream.rangeClosed(2, n).boxed()
                .collect(
                    () -> new HashMap<Boolean, List<Integer>>() {
                        private static final long serialVersionUID = 1L;
    
                        {
                            put(true, new ArrayList<Integer>());
                            put(false, new ArrayList<Integer>());
                        }
                    }
                    , (acc, candidate) -> {
                        acc.get(isPrime(acc.get(true), candidate)).add(candidate);
                    }
                    , (map1, map2) -> {
                        map1.get(true).addAll(map2.get(true));
                        map1.get(false).addAll(map2.get(false));
                    }
                );
        }
    
        public static void main(String[] args) {        
            /*
             * 6.6.2 컬렉터 성능 비교 
             */        
            long fastest = Long.MAX_VALUE;
    
            // 테스트를 10번 반복한다.
            for(int i = 0; i < 10; i++) {
                long start = System.nanoTime();
    
                // 백만 개의 숫자를 소수와 비소수로 분할한다.
                partitionPrimes(1000000);
                // partitionPrimesCustomCollector(1000000);
                // partitionPrimesWithCustomCollector(1000000);
    
                // duration을 밀리초 단위로 측정한다.
                long duration = (System.nanoTime() - start) / 1000000;
    
                // 가장 빨리 실행되었는지 확인한다.
                if(duration < fastest) {
                    fastest = duration;
                }
    
                System.out.println("Fastest execution done in " + fastest +" msecs");
            }
    
    
        }
    
    }

    요약

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

    댓글

Designed by Tistory.