본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Java8 In Action] #6 스트림으로 데이터 수집

by 돼지왕 왕돼지 2018. 12. 26.
반응형

[Java8 In Action] #6 스트림으로 데이터 수집



Java8 In Action 내용을 보며 정리한 내용입니다.

정리자는 기존에 Java8 을 한차례 rough 하게 공부한 적이 있고, Kotlin 역시 공부한 적이 있습니다.

위의 prerequisite 가 있는 상태에서 추가적인 내용만 정리한 내용이므로, 제대로 공부를 하고 싶다면 책을 구매해서 보길 권장합니다!


Average, averagingdouble, averagingint, averaginglong, binaryoperator, classification function, COLLECT, collectingAndThen, Collector, collector interface, collectors factory method, collectors static factory method, collectors.maxby, collectors.minby, Comparator, concurrent, Count, Counting, custom reducing collector, doublesummarystatistics, follectors.counting, fork join framework, GroupBy, grouping, groupingby, identity function, identity_finish, INITIAL VALUE, intsummarystatistics, java8 in action, joining, joining delimiter, longsummarystatistics, Max, maxby, Min, minby, optional, parallel reduce, parallel reduce hint, partitioning, partitioning function, partitioningby, Predicate, Reduce, reducing, reducing factory method, spliterator, StringBuilder, substream, Sum, summarization 연산, summarize, summarizingdouble, summarizingint, summarizingLong, summingdouble, summingint, summinglong, tocollection, tolist, toset, unordered, 그룹화, 변환 함수, 분할, 스트림으로 데이터 수집, 커스텀 컬렉터


6.1. 컬렉터란 무엇인가?


-

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



-

다수준(multilevel)로 그룹화를 수행할 때 명령형 프로그래밍과 함수형 프로그래밍의 차이점이 더욱 두드러진다.




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


-

스트림에 collect 를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.




6.1.2. 미리 정의된 컬렉터


-

Collectors 에서 제공하는 메서드의 기능은 크게 세 가지로 구분할 수 있다.


    스트림 요소를 하나의 값으로 리듀스하고 요약(summarize)

    요소 그룹화

    요소 분할(partitioning)





6.2. 리듀싱과 요약


-

Collectors.counting() 을 통해 갯수를 셀 수 있다.

이를 생략해서 stream 에 바로 count() 함수를 날릴 수도 있다.


counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.




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


-

Collectors.maxBy, Collectors.minBy 를 이용할 수 있다.

두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator 를 인수로 받는다.

그리고 collect 된 return 값은 Optional 이다.



-

스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산에도 리듀싱 기능이 자주 사용된다.

이런 연산을 요약(summarization) 연산이라 부른다.




6.2.2. 요약 연산


-

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

summingInt 는 객체를 int 로 매핑하는 함수를 인수로 받는다.

int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));



-

Collectors.summingLong, Collectors.summingDouble 도 summingInt 와 같은 방식으로 동작하며, 각각 long 또는 double 형식의 데이터로 요약한다는 점만 다르다.



-

Collectors.averagingInt, averagingLong, averagingDouble 등으로 평균을 구할 수 있다.



-

summarizingInt 류를 이용하면 count, sum, average, max, min 을 한번에 구할 수 있다.

return 값은 IntSummaryStatistics 이다.


summarizingLong, summarizingDouble 등이 있고 이에 매핑하는 LongSummaryStatistics, DoubleSummaryStatistics 가 있다.




6.2.3. 문자열 연결


-

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

이는 내부적으로 StringBuilder 를 이용한다.

String shortMenu = menu.stream().map(Dish::getName).collect(joining());


-

joining 은 param 으로 delimiter 를 넣어 줄 수도 있다.




6.2.4. 범용 리듀싱 요약 연산


-

당연히 앞서 본 모든 컬렉터는 reducing 팩토리 메서드로 정의할 수 있다.



-

reducing 은 세 개의 인수를 받는다.

    첫번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때 반환되는 값

    두번째 인수는 변환 함수

    세번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator.



-

한 개의 인수를 받는 reducing 도 있다.

이 경우 스트림의 3개의 인수를 받는 경우로 고려해보면, 시작요소를 첫 번째 인수로 받으며, 자신을 그대로 반환하는 항등 함수(identity function)을 두 번째 인수로 받는 상황에 해당한다.


따라서 갯수가 없으면 첫번째 인수로 넘겨줄 것이 없기 때문에 Optional 이 return 된다.



-

Stream<Integer> stream = Arrays.asList(1,2,3,4,5,6).stream();
List<Integer> numbers = 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; }

위 코드에는 의미론적인 문제와 실용성 문제 등 두 가지 문제가 발생한다.

collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드인 반면 reduce 는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적 문제가 일어난다. ( 이는 나중에 다시 다룬다 )


위의 코드에서는 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로, 리듀싱 연산을 병렬로 수행할 수 없다는 점도 문제다. ( 이도 나중에 다시 다룬다 )



컬렉션 프레임워크 유연성: 같은 연산도 다양한 방식으로 수행할 수 있다!


-

메서드 레퍼런스를 써서 더 간단히 구현할 수 있다.

int totalCalories = menu.stream().collect( reducing(0, Dish::getCalories, Integer::sum) );


-

counting 도 다음과 같이  1 로 매핑해서 구할 수 있다.

reducing(0L, e -> 1L, Long::sum);


자신의 상황에 맞는 최적의 해법 선택






6.3. 그룹화


-

groupBy 를 호출할 때 전달하는 함수를 분류 함수(classification function) 이라고 부른다.




6.3.1. 다수준 그룹화


-

Collectors.groupingBy 는 일반적인 분류 함수와 컬렉터를 인수로 받는다.

Map<Dish.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;
        }
    });


-

다수준 그룹화 연산은 다양한 수준으로 확장할 수 있다.

즉, n수준 그룹화의 결과는 n수준 트리 구조로 표현되는 n수준 맵이 된다.


보통 groupingBy 연산을 버킷 개념으로 생각하면 쉽다.

첫 번째 groupingBy 는 각 키의 버킷을 만든다. 그리고 준비된 각각의 버킷을 서브스트림 컬렉터로 채워가기를 반복하면서 n 수준 그룹화를 달성한다




6.3.2. 서브그룹으로 데이터 수집


-

Map<Dish.Type, Long> typesCount = menu.stream().collect( groupingBy(Dish::getType, counting()) );

한개의 인수를 갖는 groupingBy(f) 는 사실 groupingBy( f, toList() ) 의 축약형이다.



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


-

collector 를 통할 때 Optional 이 나오는 경우 확신이 있어 Optional mapping 을 없애고 싶다면..

collectingAndThen 을 사용하면 좋다.

Map<Dish.Type, Dish> mostCalricByType = menu.stream().collect(
        groupingBy(Dish::getType, 
                    collectingAndThen(maxBy(compaingInt(Dish::getCalories)), Optional::get)));


groupingBy 와 함께 사용하는 다른 컬렉터 예제


-

Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType = menu.stream().collect(
    groupingBy(Dish::getType, mapping(dish -> {
        if(dish.getCalories() <= 400){
            return CaloricLevel.DIET;
        } else if( dish.getCalories() <= 700){
            return CaloricLevel.NORMAL;
        } else{
            return CaloricLevel.FAT;
        }
    }, toCollection(HashSet::new)))
);





6.4. 분할


-

분할은 분할 함수(partitioning function)라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.

Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));



6.4.1. 분할의 장점


-

참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 장점이다.

컬렉터를 두 번째 인수로 전달할 수 있는 오버로드된 버전의 partitioningBy 메서드도 있다.

Map<Boolean, Map<Dish.Type, List<DIsh>> vegetarianDishesByType = menu.stream().collect(

    partitioningBy(Dish::isVeritarian, groupingBy(Dish::getType)));



6.4.2. 숫자를 소수와 비소수로 분할하기


-

Collectors 클래스의 정적 팩토리 메서드


toList -> List<T>

toSet -> Set<T>

toCollection -> Collection<T>

counting -> Long

summingInt -> Integer

averagingInt -> Double

summarizingInt -> IntSummaryStatistics

joining -> String

maxBy -> Optional<T>

minBy -> Optional<T>

reducing -> 연산에서 형식을 결정

collectingAndThen -> 반환 함수가 형식을 반환

groupingBy -> Map<K, List<T>>

partitioningBy -> Map<Boolean, List<T>>





6.5. Collector 인터페이스


-

Collector 인터페이스는 리듀싱 연산(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.

우리가 Collector 인터페이스를 구현하는 리듀싱 연산을 만들 수도 있다.

public interface Collector<T, A, R>{
    Supplier<A> supplier();
    BiConsume<A, T> accumulator();
    Function<A, R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
}

T는 수집될 스트림 항목의 제네릭 형식

A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.

R은 수집 연산 결과 객체의 형식(항상 그런 것은 아니지만 대게 컬렉션 형식)이다.




6.5.1. Collector 인터페이스의 메서드 살펴보기


-

아래에 있는 것은 ToListCollector 의 구현 내용이다.



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


-

supplier 메서드는 빈 결과로 이루어진 Supplier 를 반환해야 한다. 즉, supplier 는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.

public Supplier<List<T>> supplier(){
    return () -> new ArrayList<T>();
    // ArrayList::new;
}



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


-

accumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환한다. 스트림에서 n번째 인수를 탐색할 때 두 인수, 즉 누적자(스트림의 첫 n-1개 항목을 수집한 상태)와 n번째 요소를 함수에 적용한다.

함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.

public BiConsumer<List<T>, T> accumulator(){
    return (list, item) -> list.add(item);
    // List::add;
}



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


-

finisher 메서드는 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.

때로는 누적자 자체가 이미 최종 결과인 상황도 있다.

public Function<List<T>, List<T>> finisher(){
    return Function.identity();
}






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


-

combiner 는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.

public BinaryOperator<List<T>> combiner(){
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    }
}


-

스트림의 리듀싱을 병렬로 수행할 때 자바7의 포크/조인 프레임워크와 Spliterator 를 사용한다.

병렬 리듀싱 수행 과정은 다음과 같다.


스트림을 분할해야 하는지 정의하는 조건이 거짓으로 바뀌기 전까지 원래 스트림을 재귀적으로 분할한다. ( 보통 분산된 작업의 크기가 너무 작아지면 병렬 수행의 속도는 순차 수행의 속도보다 느려진다. 즉, 병렬 수행의 효과가 상쇄된다. 일반적으로 프로세싱 코어의 개수를 초과하는 병렬 작업은 효율적이지 않다.)


서브 스트림(substream)의 각 요소에 리듀싱 연산을 순차적으로 적용해서 서브스트림을 병렬로 처리할 수 있다.


마지막에는 컬렉터의 combiner 메서드가 반환하는 함수로 모든 부분결과를 쌍으로 합친다. 즉, 분할된 모든 서브스트림의 결과를 합치면서 연산이 완료된다.



Characteristics 메서드


-

Characteristics 는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.

이는 다음 세 항목을 포함하는 열거형이다.


UNORDERED

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


CONCURRENT

    다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다.

    컬렉터의 플래그에 UNORDERED 를 함께 설정하지 않았다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.


IDENTITY_FINISH

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




6.5.2. 응용하기


컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기


-

IDENTITY_FINISH 수집 연산에서는 Collector 인터페이스를 완전히 새로 구현하지 않고도 같은 결과를 얻을 수 있다.

Stream 은 세 함수(supplier, accumulator, combiner)를 인수로 받는 collect 메서드를 오버로드하며 각각의 메서드는 Collector 인터페이스의 메서드가 반환하는 함수와 같은 기능을 수행한다.

List<Dish> dishes = menuStream.collect( ArrayList::new, List::add, List::addAll )

위의 collector 는 IDENTITY_FINISH 와 CONCURRENT 지만 UNORDERED 는 아닌 컬렉터로만 동작한다.





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


6.6.1. 소수로만 나누기




6.6.2. 컬렉터 성능 비교





6.7. 요약


-

collect 는 스트림의 요소를 요약 결과로 누적하는 다양한 방법(컬렉터라 불리는)을 인수로 갖는 최종 연산이다.



-

스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최솟값, 최댓값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있다.



-

미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할할 수 있다.



-

컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.



-

Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다.





반응형

댓글