chapter6 - 스트림으로 데이터 수집

  • 람다가 없다면
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();

for (Transaction transaction : transactions) {
    Currency currency = transaction.getCurrency();
    List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
    if (transactionsForCurrency == null) {
            transactionsForCurrency = new ArrayList<>();
        transactionsByCurrencies.put(currency, transactionsForCurrency);
    }
    transactionsForCurrency.add(transaction);
}

System.out.println(transactionsByCurrencies);
  • java 8에서는
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));

System.out.println(transactionsByCurrencies);

6.1 컬렉터란 무엇인가?

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

  • collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있다는 점.
  • Collector 인터페이스의 메서드를 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정된다.
  • Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.
    List<Transaction> transactions = transactionStream.collect(Collectors.toList());
    

6.1.2 미리 정의된 컬렉터

  • Collectors 제공 메서드
    • 스트림 요소를 하나의 값으로 리듀스하고 요약
    • 요소 그룹화
    • 요소 분할

6.2 리듀싱과 요약

  • 카운팅

      import static java.util.stream.Collectors.*;
    
      long howManyDishes = menu.stream().collect(counting());
      howManyDishes = menu.stream().count();
    

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

    Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
    Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
    mostCaloriesDish = menu.stream().collect(minBy(dishCaloriesComparator));
    

    6.2.2 요약 연산

  • 숫자 필드의 합계나 평균등을 반환하는 연산.
  • 합계 : summingInt, summingDouble, summingLong

    int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
    
  • 평균 : averagingInt,averaging, averagingDouble

    double avgCalories = menu.stream().collect(averagingInt(Dish::getCalories));
    
  • 다중값 ( 카운터, 합계, 최소값, 평균, 최대값 ) : summarizingInt, summarizingLong, summarizingDouble

    IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
    

6.2.3 문자열 연결

  • 문자열을 join시키기..
    String shortMenu = menu.stream().map(Dish::getName).collect(joing(","));
    

    6.2.4 범용 리듀싱 요약 연산

  • 프로그램의 가독성을 위해 Collectors.reducing을 추천한다.
    int totalCalories = menu.stream().collect( reducing( 0, Dish::getCalories,(i,j)->i+j ));
    
  • 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때 반환값이다.(숫자 합계에서는 인수가 없을 때 반환값으로 0이 적합하다)
  • 두 번째 인수는 사용되어질 값.
  • 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.

  • max 값 구하기

    Optional<Dish> mostCalorieDish = menu.stream()
          .collect(reducing( (d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2 ));
    
  • reduce의 잘못된 예

    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는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 발생.
  • 실용성 문제 : 여러 스레드가 동시에 같은 데이터 구조체를 고침ㄴ 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 점도 문제. 이 문제를 해결하려면 매번 새로운 리스트를 할당해야 하고 따라서 객체를 할당하느라 성능이 저하될 것이다.
  • 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직 하다.

  • 컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
  • 리듀싱 사용한 합계
int totalCalories = menu.stream().collect(reducing(0, //초깃값 
                                              Dish::getCalories, //변환함수 
                                              Integer::sum));    //합계함수
  • counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서 구현 할 수 있다.
    public static <T> Collector<T,?,Long>counting(){
      return reducing(0L, e -> 1L, Long::sum);
    }
    
    int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum)
                          .get(); //Optional<Integer>를 get으로 값을 가져옴. 이것보다 orElse, orElseGet 으로 쓰는것이 좋음.
    
    int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
    

6.3 그룹화

enum CaloricLevel { DIET, NORMAL, FAT };
Map<CaloricLevel, List<Dish>> result = menu.stream().collect(
                groupingBy(dish -> {
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT;
                } ));

6.3.1 다수준 그룹화


Map<Dish.Type, Map<CaloricLevel, List<Dish>>> result = menu.stream().collect(
                groupingBy(Dish::getType,
                        groupingBy((Dish dish) -> {
                            if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                            else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                            else return CaloricLevel.FAT;
                        } )
                )
        );

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

  • 요리 종류별 갯수
    Map<Dish.Type, Long> typesCount = menu.stream().collect(groupingBy(Dish::getType,counting());
    
  • 분류별 칼로리가 가장 높은 음식
    Map<Dish.Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));
    
  • 컬렉터 결과를 다른 형식에 적용하기

    Map<Dish.Type, Dish> mostCaloricByType = 
            menu.stream()
                .collect( groupingBy( Dish::getType, //분류 
                                  collectingAndThen( //maxBy를 사용하면 Optional이 반환됨으로...
                                    maxBy(comparingInt(Dish::getCalories)),
                                  Optional::get)));//get메소드로 Dish를 반환받는다.
    
  • groupingBy와 함께 사용하는 다른 컬렉터 예제

  • summingInt : 합계

    Map<Dish.Type, Integer> totalCaloriesByType = menu.stream().collect(groupingBy(Dish::getType,summingInt(Dish::getCalories)));
    
  • mapping : 누적


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; },
                        toSet() )));

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 분할

  • 분할 함수 : boolean을 키로 사용하는 그룹화
Map<Boolean, List<Dish>> partitionedMenu = 
          menu.stream().collect(partitioningBy(Dish::isVegetrian));

6.4.1 분할 장점

  • 반대의 것을 얻을수 있다.

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

    public static Map<Boolean, List<Integer>> partitionPrimes(int n) {
        return IntStream.rangeClosed(2, n).boxed()
                .collect(partitioningBy(candidate -> isPrime(candidate)));
    }

    public static boolean isPrime(int candidate) {
        return IntStream.rangeClosed(2, candidate-1)
                .limit((long) Math.floor(Math.sqrt((double) candidate)) - 1)
                .noneMatch(i -> candidate % i == 0);
    }
  • Collectors Class : http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collectors.html
    팩토리 메서드 반환형식 사용 예제
    toList List<T> 스트림의 모든 항목을 리스트로 수집.
    List<Dish> dishes = menuStream.collect(toList());
    toSet Set<T> 스트림의 모든 항목을 중복이 없는 집합으로 수집
    Set<Dish> dishes = menuStream.collect(toSet());
    toCollection Collection<T> 스트림의 모든 항목을 공급자가 제공하는 컬렉션으로 수집
    Collection<Dish> dishes = menuStream.collect(toCollection(), ArrayList::new);
    counting Long 스트림의 항목 수 계산.
    long howManyDishes = menuStream.collect(counting());
    summingInt Integer 스트림의 항목에서 정수 프로퍼티값을 더함
    int totalCalories = menuStream.collect(summingInt(Dish::getCalories));
    averagingInt Double 스트림 항목의 정수 프로퍼티의 평균값 계산.
    double avgCalories = menuSream.collect(averagingInt(Dish::getCalories));
    summarizingInt IntSummaryStatistics 스트림 내의 항목의 최대값, 최솟값, 합계, 평균등의 정수 정보 통계를 수집
    IntSummaryStatistics menuStatistics = menuStream.collect(summarizingInt(Dish::getCalories));
    joining String 스트림의 각 항목에 toString 메서드를 호출한 결과 문자열을 연결
    String shortMenu = menuStream.map(Dish::getName).collect(joining(", "));
    maxBy Optional<T> 주어진 비교자를 이용해서 스트림의 최댓값 요소를 Optional로 감싼 값을 반환.스트림에 요소가 없을 때는 Optional.empty()를 반환.
    Optional<Dish> fattest = menuStream.collect(maxBy(comparingInt(Dish::getCalories)));
    minBy Optional<T> 주어진 비교자를 이용해서 스트림의 최솟값 요소를 Optional로 감싼 값을 반환. 스트림에 요소가 없을때는 Optional.empty()를 반환.
    Optional<Dish> lightest = menuStream.collect(minBy(comparingInt(Dish::getCalories)));
    reducing 리듀싱 연산에서 형식을 결정 누적자를 초깃값으로 설정한 다음에 binaryOperator로 스트림의 각 요소를 반복적으로 누적자와 합쳐 스트림을 하나의 값으로 리듀싱.
    int totalCalories = menuStream.collect(reducing(0,Dish::getCalories, Integer::sum));
    collectingAndThen 변환 함수가 형식을 반환 다른 컬렉터를 감싸고 그 결과에 변환 함수를 적용.
    int howManyDishes = menuStream.collect(collectingAndThen(toList(), List::size));
    groupingBy Map<K,List<T>> 하나의 프로퍼티 값을 기준으로 스트림의 항목을 그룹화하며 기준 프로퍼티값을 결과 맵의 키로 사용.
    Map<Dish.Type,List<Dist> dishesByType = menuStream.collect(groupingBy(Dish::getType));
    partitioningBy Map<Boolean,List<T>> 프레디케이트를 스트림의 각 항목에 적용한 결과로 항목을 분할.
    Map<Boolean,List<Dish>> vegetarianDishes = menuStream.collect(partitioningBy(Dish::isVegetarian));

6.5 Collector 인터페이스

  • http://docs.oracle.com/javase/8/docs/api/java/util/stream/Collector.html
    public interface Collector<T, A, R>{
    Supplier<A> supplier();
    BiConsumer<A,T> accoumulator();
    Function<A,R> finisher();
    BinaryOperator<A> combiner();
    Set<Characteristics> characteristics();
    }
    
  • T는 수집될 스트림 항목의 제네릭 형식이다.
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
  • R은 수집연산 결과 객체의 형식(항상 그런것은 아니지만 대개 컬렉션 형식)이다.

  • 예를 들어 Stream<T>의 모든 요소를 List<T>로 수집하는 ToListCollector<T>라는 클래스를 구현할 수 있다.

    public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
    

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

  • supplier method : 새로운 결과 컨테이너 만들기. = 빈 누적자를 생성한다.
    public Supplier<List<T>> supplier(){
    return () -> new ArrayList<T>();
    }
    
    or
    public Supplier<List<T>> supplier(){
    return ArrayList::new;
    }
    
  • accumulator 메소드 : 결과 컨테이너에 요소 추가하기. = 리듀싱 연산을 수행하는 함수를 반환. 누적자에 값을 추가.
    public BiConsumer<List<T>,T> accumulator(){
    return (list, item) -> list.add(item);
    }
    
    or
    public BiComsumer<List<T>,T> accumulator(){
      return List::add; 
    }
    
  • finisher 메소드 : 최종 변환값을 결과 컨테이너로 적용하기.
    public Function<List<T>,List<T>> finisher(){
      return Function.identity();
    }
    
  • combiner 메서드 : 두 결과 컨테이너 병합 (병렬처리).
    public BinaryOperator<List<T>> combiner(){
      return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
      }
    }
    
  • Characteristics 메서드 : : 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스 한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.
    • UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
    • CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이컬렉터는 스트림의 병렬 리듀싱을 수행 할수 있다. 컬렉터 플래그에 UNORDERED가 선언되지 않는다면 무순서 병렬 리듀싱을 수행 할 수 있다.
    • IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 이를 생략할 수 있다. 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 할수 있다. 또한 누적자 A를 결과 R로 안전하게 형 변환할 수 있다.

6.5.2 응용하기

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {

    @Override
    public Supplier<List<T>> supplier() {
        return () -> new ArrayList<T>(); //수집 연산의 시발점.
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item); // 탐색한 항목을 누적하고 바로 누적자를 고친다.
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity()//i -> i; //항등 함수 
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2); // 두번째 콘텐츠와 합쳐서 첫번째 누적자를 고친다.
            return list1;  // 변경된 첫 번째 누적자를 반환한다.
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        // 콜렉터의 플래그를 IDENTITY_FINISH, CONCURRENT로 설정한다.
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
    }
}
  • 사용
    List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>());
    
  • 컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기 (IDENTITY_FINISH와 CONCURRENT지만 UNORDERED가 아닌.)
    List<Dish> dishes = menuStream.collect(
      ArrayList::new, //supplier
      List::add,      // accumulator
      List::addAll    // combiner
    );
    

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

    6.6.1 소수로만 나누기