Java8 In Action

[Part2] Java8 In action - Chapter6 - 2

신나게개발썰 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 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다.
반응형