ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 챕터[6] 스트림 그룹화
    모던자바인액션 2020. 10. 21. 15:58

    서론

    스트림을 이용한 데이터 그룹화


    // 최대 칼로리 Dish 찾기
    Comparator<Dish> dishD =
    Comparator.comparingInt(Dish::getCalories);
    
    Optional<Dish> oD = specialMenu.stream().collect(Collectors.maxBy(dishD));

    요약연산 : 스트림에 있는 객체의 숫자 필드의 합계, 평균 등을 반환해주기 위해 리듀싱 기능을 사용하는 연산

    // Dish 총 칼로리 구하기
    // summingInt 에서는 리듀스 연산이 이루어지며 초기값은 0이 된다.
    int totalCal = specialMenu.stream().collect(Collectors.summingInt(Dish::getCalories));
    // 칼로리 평균 구하기
    // averageingInt 이름에 리턴형을 int로 주어선 안된다.
    double avgCal = specialMenu.stream().collect(Collectors.averagingInt(Dish::getCalories));
    // summarizingInt ( 컨텐츠 숫자, 합계, 최소값, 최대값, 평균 ) 값을 얻어옴
    IntSummaryStatistics menuD = specialMenu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
    // 문자열 연결하기 toString이 없을경우 map을 이용하여 이름 덩어리를 만들어낸다.
    String shortName = specialMenu.stream().map(Dish::getName).collect(Collectors.joining());
    
    // 문자열 연결 toString이 있으면 map을 통해 이름 덩어리를 만드는 과정 생략이 가능하다는데..
    // String shortName2 = specialMenu.stream().collect(Collectors.joining());
    
    // 문자열을 그냥 연결이 아닌 특정문자열로 나누기 (", ")
    String shortName2 = specialMenu.stream().map(Dish::getName).collect(Collectors.joining(", "));

     

    함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결할 수 있음을 보여주며, 스트림 인터페이스에서 직접 제공하는 메소드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡하다는 사실도 보여준다.
    코드가 좀 더 복잡한 대신 재사용성과 커스터마이징 가능성을 제공하는 높은 수준의 추상화와 일반화를 얻을 수 있다.
    문제 해결책은 문제에 특화된 방법을 고르는게 제일 좋다. 예를들어 메뉴의 전체 칼로리를 계산하는 예제에서는 IntStream을 이용하여 자동 언박싱 연산을 수행하거나 Integer -> int로 변환하는 과정을 피할 수 있어 성능까지 좋다.

     

    그룹화

    데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산도 데이터베이스에서 많이 수행되는 작업이다. 트랜잭션 통화 그룹화 예제에서 확인했듯이 명령형으로 그룹화를 구현하려면 까다롭고 할일많고 에러가 많이 발생한다. 하지만 자바 8의 함수형을 이용하면 가독성 있는 한줄의 코드로 그룹화가 가능하다.

     

    //Dish.Type 별로 그룹화 하기
    Map<Dish.Type, List<Dish>> dishType =
    			specialMenu.stream().collect(Collectors.groupingBy(Dish::getDishType));
    // 다수준 그룹화
    // Dish.Type 끼리 그룹화 후 칼로리의 숫자별로 추가로 다이어트, 노말, 뚱뚱함을 나눠준다.
    Map<Dish.Type, Map<Dish.CaloricLevel, List<Dish>>> dishesByTypeCaloricLevel = 
    				specialMenu.stream().collect(Collectors.groupingBy
                    (Dish::getDishType, Collectors.groupingBy(dish -> {
                                    if (dish.getCalories() <= 200)
                                    return Dish.CaloricLevel.DIET;
                                    else if (dish.getCalories() <= 400)
                                    return Dish.CaloricLevel.NORMAL;
                                    else
                                    return Dish.CaloricLevel.FAT;}
    )));
    // 서브그룹으로 데이터 수집
    // Dish.Type 끼리 그룹화 하여 그 그룹이 몇개있는지 알아낸다.
    Map<Dish.Type, Long> countDish = 
    					specialMenu.stream().collect(
                        Collectors.groupingBy(Dish::getDishType, Collectors.counting()));
    // 그룹화 결과중 최대값 ( 언제나 존재유무가 의심되는 행동은 Optional로 리턴한다)
    Map<Dish.Type, Optional<Dish>> mostCaloricByType = specialMenu.stream()
    							.collect(Collectors.groupingBy(Dish::getDishType, Collectors
    							.maxBy(Comparator.comparingInt(Dish::getCalories))));
    // 그룹화 결과중 최대값 ( Optional 객체로 리턴 안하는 방법 )
    Map<Dish.Type, Dish> mostCalByMax = specialMenu.stream().collect(Collectors.groupingBy
    						(Dish::getDishType, Collectors.collectingAndThen(
    						Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)), Optional::get)));
    						mostCalByMax.forEach((type, dish) -> System.out.println(dish));

    분할함수

    분할함수는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 쉽게 말하자면 참, 거짓으로 그룹을 나눈다는 의미이다. 

    장점으로는 참, 거짓 두 가지 요소의 스트림 리스트를 모두다 유지하는게 가능하다.

     

    Collector 인터페이스


    public interface Collector<T, A, R> {
            Supplier<A>> supplier();
            BiConsumer<A, T> accumulator();
            Function<A, R> finisher();
            BinaryOperator<A> combiner();
            Set<Characteristics> characteristics();
    }
    T: 수집될 스트림 항목의 제너릭 형식
    A: 누적자(수집 과정에서 중간 결과를 누적하는 객체형식)
    R: 수집연산 결과 객체의 형식(주로 컬렉션 형식)

     

    supplier 메서드 : 새로운 결과 컨테이너 만들기

    supplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 한다. 즉 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수이다. ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.

     

    accumulator 메서드 : 결과 컨테이너에 요소 추가하기

    accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다. 스트림에서 n번째 요소를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다. 함수의 반환값을 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다. ToListCollector에서 accumulator가 반환하는 함수는 이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산을 수행한다.

     

    finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기

    finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다. 때로는 ToListCollector에서 볼 수 있는 것처럼 누적자 객체가 이미 최종 결과인 상황도 존재한다. 이런 때는 변환 과정이 필요하지 않으므로 finisher 메소드는 항등 함수를 반복한다.

    * 항등함수란, 쉽게말해 T인자를 받으면 T 객체를 반환하는 함수 인터페이스 (함수는 한개인데, 제네릭을 통해 여러 타입을 적용가능)

     

    combiner 메서드 : 두 결과 컨테이너 병합

    마지막으로 리듀싱 연산에서 사용할 함수를 반환하는 네 번째 메서드 combiner는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다. toList의 combiner는 비교적 쉽게 구현할 수 있다. 즉, 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다.

    이걸 이용하면 스트림의 리듀싱을 병렬로 수행할 수 있다.

     

    Characteristice 메서드

    Characteristice 메서드는 컬렉터 연산을 정의하는 Characteristice 형식의 불변 집합을 반환한다. Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다. Characteristice는 아래 세 항목을 포함하는 열거형이다.

    * 열거형(enum)

    UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.

    CONCURRENT : 다중 스레드에서 accmulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED를 함께 설정하지 않았다면 데이터 소스가 정열되어 있지 않는(즉, 집합처럼 요소의 순서가 무의미한) 상황에서만 병렬 리듀싱을 수행할 수 있다.

    IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순한 identity를 적용할 뿐 이므로 이를 생략할 수 있다. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.

     

     

    '모던자바인액션' 카테고리의 다른 글

    챕터[8] 컬렉션 API 개선  (0) 2020.10.21
    챕터[7] 스트림 병렬화  (0) 2020.10.21
    챕터[5] 스트림 데이터 수집  (0) 2020.10.21
    챕터[4] 스트림  (0) 2020.10.21
    챕터[3] 람다  (0) 2020.10.21

    댓글

Designed by Tistory.