• 이 장의 내용

    • Collectors 클래스로 컬렌션을 만들고 사용
    • 하나의 값으로 데이터 스트림 리듀스하기
    • 특별한 리듀싱 요약 연산
    • 데이터 그룹화와 분할
    • 자신만의 커스텀 컬렉터 개발
  • collect와 컬렉터로 구현할 수 있는 질의 예제

    • 통화별로 트랜잭션을 그룹화한 다음 해당 통화로 일어난 모든 트랜잭션 합계를 계산 → return Map<Currency, Integer>

    • 트랜잭션을 비싼/저렴한 트랜잭션으로 분류 → return Map<Boolean, List<Transaction>>

    • 트랜잭션을 도시 등 다수준으로 그룹화, 각 트랜잭션이 비싼/저렴한 구분 → return Map<String, Map<boolean, List<Transaction>>>

    • 예시 코드

      // 그룹화한 트랜잭션을 저장할 맵 생성
      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<>();
      		transactionsForCurrencies.put(currency, transactionsForCurrency);
      	}
      	// 같은 통화를 가진 트랜잭션 리스트에 현재 탐색 중인 트랜잭션을 추가
      	transactionsForCurrency.add(transaction);
      }
      
      // 스트림을 활용해 같은 예제 간단하게 구현 가능
      Map<Currency, List<Transaction>> transactionsByCurrencies = 
      	transactions.stream().collect(groupingBy(Transaction::getCurrency));
      
  • 컬렉터란

    • collect() 메서드를 이용해 Collector 인터페이스의 구현체를 전달

    • 보통 collect(toList())메서드를 통해 Collector 인터페이스 구현제를 사용해 각 요소를 리스트로 만들도록 사용

    • 고급 리듀싱을 수행하는 컬렉터

      collect 메서드의 내부적 리듀싱 연산

      collect 메서드의 내부적 리듀싱 연산

      • collect로 결과를 수집하는 과정을 간단하면서 유연한 방식으로 정의할 수 있다는 점이 컬렉터의 장점
      • 구체적으로는 스트림에 collect() 메서드를 호출하면 스트림의 요소에 리듀싱 연산이 수행됨
    • 미리 정의된 컬렉터

      • 스트림 API에서는 groupingBy 처럼 미리 정의된 메서드 제공
      • 이처럼 Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분
        • 스트림 요소를 하나의 값으로 리듀스하고 요약 → count(), sum() 등
        • 요소 그룹화 → groupingBy() 등
        • 요소 분할 → boolean을 반환하는 함수로 그룹화
    • 리듀싱과 요약

      • counting 활용

        long howManyDishes = menu.stream().collect(Collectors.counting());
        
        // 아래와 같이 간단하게 요약 가능
        // import static java.util.stream.Collectors.* 했다고 가정
        long howManyDishes = menu.straem().count();
        
      • 최댓갑과 최솟값 검색

        • Collectors.maxBy, Collectors.minBy 메서드를 이용해 최솟값/최댓값 계산 가능

        • 두 컬렉터는 비교하는 데 사용할 Comparator를 인수로 받음

        • 예시 코드

          // 가장 높은 칼로리 메뉴를 찾는 예시 코드
          Comarator<Dish> dishCaloriesComparator = 
          	Comparator.comparingInt(Dish::getCalories);
          
          Optional<Dish> mostCalorieDish = 
          	menu.stream()
          		.collect(maxBy(dishCaloriesComparator));
          
      • 요약 연산

        summingInt 계산 과정

        summingInt 계산 과정

        • 합계, 평균 등을 반환하는 데에도 리듀싱 연산 기능이 사용 → 요약 연산이라고 부름

        • Collectors.summingInt 메서드는 객체를 int로 매핑하는 함수를 인수로 받아 해당 값을 모두 더해서 반환

        • summingDouble도 있는데 double에 대해 처리한다는 점만 다름

        • 예시 코드

          Untitled

          Untitled

          // 메뉴의 각 칼로리를 더해서 반환
          int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
          
          // 메뉴의 칼로리 평균 반환
          double avgCalories = 
          	menu.stream().collect(averagingInt(Dish::getCalories));
          
          // 만약 하나의 연산으로 합계, 평균을 구해야 하는 경우 종합 결과를 반환
          IntSummaryStatistics menuStatistics = 
          	menu.stream().collect(summarizingInt(Dish::getCalories));
          
          // result -> IntSummaryStatistics{count=9, sum=4300, min=120, ...}
          
      • 문자열 연결

        • 컬렉터의 joining() 메서드를 이용하면 각 객체에 toString() 메서드 호출하여 추출한 문자열을 하나의 문자열로 연결해서 출력

        • joining() 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만듦

        • 예시 코드

          // 구분자 없이 연결
          String shortMenu = menu.stream().map(Dish::getName).collect(joining());
          
          // 구분자를 사용한 연결
          String shotMenu = menu.stream().map(Dish::getName).collect(joining(", "));
          
      • 범용 리듀싱 요약 연산

        • 지금까지의 내용은 reducing() 메서드로도 정의 가능

        • 앞선 특화된 컬렉터를 사용하는 이유는 편의성 때문(가독성도 중요)

        • reduce와 reducing 차이 찾아보기 → reduce 사용하면 누적자로 사용된 리스트 변환?

        • 예시 코드

          // reducing을 활용한 모든 메뉴 칼로리의 합 계산
          // 첫 번쨰 인수는 초깃값
          // 두 번쨰 인수는 각 스트림 요소의 매핑 함수
          // 세 번째 인수는 BinaryOperator 함수형 인터페이스
          int totalCalories = menu.stream()
          	.collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
          
          // 간소화 코드
          int totalCalories = menu.stream()
          	.collect(reducing(0, Dish::getCalories, Integer::sum));
          
  • 그룹화

    Untitled

    • 특정 데이터를 기준으로 그룹화하는 데 사용

    • 예시 코드

      // 간단한 사용 예제
      Map<Type, List<Dish>> dishesType = 
      	menu.stream().collect(groupingBy(Dish::getType));
      
      // 좀 더 복잡한 사용 예제
      public enum CaloricLevel = { DIET, NORMAL, FAT };
      Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream()
      	.collect(groupingBy(dish -> {
      		if (dish.getCalories() <= 400) return CaloricLevel.DIET;
      		else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
      		else return CaloricLevel.FAT;
      	}));
      
      // 요소를 그룰화한 후에는 조작하는 연산이 필요 -> 예) 500칼로리 넘는 요리만 필터링
      // 아래 예제를 통해 해결할 수 있을 것처럼 보이지만 Fish Type 메뉴가 필터링 되면 분류도 사라짐
      Map<Type, List<Dish>> caloricDishesType = menu.stream()
      	.filter(dish -> dish.getCalories() > 500)
      	.collect(groupingBy(Dish::getCalories));
      
      // 위 이슈 해결을 위해 groupingBy는 Predicate를 인자로 받음
      Map<Type, List<Dish>> caloricDishesByType = menu.stream()
      	.collect(groupingBy(Dish::getType,
      			filtering(dish -> dish.getCalries() > 500,
      			toList())));
      
      // mappping 메서드를 통해 List<String> 으로 변환 가능
      Map<Type, List<String>> dishNameByType = menu.stream()
      	.collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
      
      // flatMapping을 통해 2차원 -> 1차원 변환 가능
      Map<Type, Set<String>> dishNamesByType = menu.stream()
      	.collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));
      
    • 다수준 그룹화

      • 두 인수를 받는 Collectors.groupingBy() 를 이용하면 항목을 다수준으로 그룹화 가능

      • Collectors.groupingBy() 는 일반적인 분류 함수와 컬렉터를 인수로 받음

      • 즉, 두 번쨰 인자로 Collectors.groupingBy() 를 전달할 수 있다는 것

      • 예시 코드

        // 결과로 두 수준의 맵이 만드는 코드
        Map<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;
        			})
        		)
        );
        
    • 서브 그룹으로 데이터 수집

      • groupingBy() 메서드의 두 번쨰 인자로 전달하는 Collectors의 제한은 없음

      • 예를 들면 Collectors.couning()을 전달해서 그룹별 개수로 데이터 수집 가능

      • 한 개의 인수를 갖는 groupingBy(f)는 사실 groupingBy(f, toList())의 함축형

      • 예시 코드

        Map<Type, Long> typesCount = menu.stream()
        	.collect(groupingBy(Dish::getType, counting());
        
        // result -> {MEAT=3, FISH=2, OTHER=4}
        
        // 요리를 Type으로 구분하고 각 Type별 가장 높은 칼로리 메뉴 추출
        Map<Dish, Optional<Dish>> mostCaloricByType = 
        	menu.stream()
        		.collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
        
    • 컬렉터 결과를 다른 형식에 적용하기

      • 마지막 그룹화 연산에서 맵의 모든 값을 Optional로 감쌀 필요가 없으므로 Optional을 삭제할 수 있음

      • Collectors.collectingAndThen() 으로 컬렉터가 반환한 결과를 다른 형식으로 활용 가능

      • collectingAndThen()은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환

      • 예시 코드

        Map<Type, Dish> mostCaloricByType = menu.stream()
        	.collect(groupingBy(Dish::getType,
        		collectingAndThen(
        			maxBy(comparingInt(Dish::getCalories)),
        			Optional::get))):
        
    • groupingBy와 함께 사용하는 다른 컬렉터 예제

      • 일반적으로 스트림에서 같은 그룹으로 분류된 모든 요소에 리듀싱 작업을 수행할 떄는 팩토리 메서드 groupingBy() 에 두 번쨰 인수로 전달한 컬렉터 활용

      • 예를 들어 메뉴에 있는 모든 요리의 칼로리 합계를 구하려고 만든 컬렉터 재사용 가능

      • 예시 코드

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

    • 분할은 분할 함수로 불리는 Predicate를 분류 함수로 사용하는 특수한 그룹화 기능

    • 분할 함수는 boolean을 반환하므로 맵의 키는 boolean으로 이루어짐 → 그룹은 최대 2개만 가능

    • 예시 코드

      // 앞선 groupingBy와 가장 큰 차이점은 Predicate를 첫 번쨰 인수로 받는다는 점
      Map<Boolean, List<Dish>> partitionedMenu = menu.stream()
      	.collect(partitioningBy(Dish::isVegetarian));
      
      // Map응 이용해 채식 요리를 얻을 수 있음
      List<Dish> vegetarianDishes = partitionedMenu.get(true);
      
    • 분할의 장점

      • 분할 함수는 반환하는 참/거짓을 통해 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이 분할의 장점
      • 또한 partitioningBy() 는 두 번쨰 인수로 Collectors를 전달할 수 있는 오버라이딩된 함수도 존재
      • 예시 코드
  • Collectors 클래스의 정적 팩토리 메서드 정리

  • Collectors 인터페이스

  • 232P 다시 보기