Skip to content

Latest commit

 

History

History
176 lines (122 loc) · 6.32 KB

06장(스트림으로 데이터 수집).md

File metadata and controls

176 lines (122 loc) · 6.32 KB

스트림으로 데이터 수집

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

컬렉션(Collection), 컬렉터(Collector), collect는 서로 다르다.

컬렉터란 무엇인가?

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

스트림에 collect를 호출하면 스트림의 요소에(컬렉터로 파라미터화된) 리듀싱 연산이 수행된다. 즉, 내부적으로 리듀싱 연산이 일어난다.

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

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

reducing

범용 Collectors.reducing을 사용

int totalCalories = menu.stream()
  .collect(reducing(0, Dish::getCaloreis, (i, j) -> i + j));

reducing은 세 개의 인수를 받는다. (초기값, 합계 함수, 변환 함수)

  • 첫 번째 인수는 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때는 반환값이다.(숫자 합계에서는 인수가 없을 때 반환하므로 0이 적합하다.)
  • 두 번째 인수는 함수를 받는다.
  • 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator이다.

한 개의 인수를 갖는 reducing

가장 칼로리가 높은 요리 찾는 방법

Optional<Dish> mostCaloireDish = menu.stream().collect(reducing((d1, d2) -> d1.getCaloreis() > d2.getCalories() ? d1 : d2));

한 개의 인수를 갖는 reducing 팩터리 메서드는 세 개의 인수를 갖는 reducing 메서드에서 첫 번째 인수를 받고, 두 번째 인수에서 자기 자신을 그대로 반환하는 항등함수(identity function)를 두 번째 인수로 받는 상황에 해당한다.

따라서 한 개의 인수를 갖는 reducing 컬렉터는 시작값이 없으므로 빈 스트림이 넘겨졌으래 시작값이 설정되지 않아 null을 반환할 수 있으므로 Optional 객체로 만들어 사용해야 한다.

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

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

그룹화

자바8의 함수형을 이용하면 가독성 있는 한 줄의 코드로 그룹화를 구현할 수 있다.

/** 
  * 그룹화 groupingBy
  * 생선, 고기 그 밖의 것들로 그룹화 
  */
Map<Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));

groupingBy를 분류 함수(classification function) 이라고 한다.

/**
  * 칼로리별로 그룹화
  */
  
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칼로리가 넘는 요리만 타입과 종류로 그룹화
Map<Type, List<Dish>> caloricDishesByType = menu.stream().filter(dish -> dish.getCalroies() > 500)
  .collect(groupingBy(Dish::getType));
/**
  * 결과
  * {OTHER=[french fries, pizza], MEAT=[pork, beef]}
  * 위 코드의 단점은 위 filter 프레디케이트를 만족하는 값이 없을 경우 키값 자체가 제외되서 맵에 담지 못한다.
  * 해결책 : Collectors 클래스의 정적 팩터리 메서드인 filtering 사용
  */
  
// 해결
Map<Type, List<Dish>> caloricDishesByType = menu.stream().collect(groupingBy
  (Dish::getType, filtering(dish -> dish.getCalories() > 500, toList())));
// 결과 : {OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}

/** mapping 사용 */
Map<Type, List<String>> dishNamesByType = menu.stream().
  collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
  
/** flatMapping 사용 
  * flatMap은 두 수준의 리스트를 한 수준으로 평면화 할 수 있음 
  */
Map<Type, Set<String>> dishNamesByType = 
  menu.stream()
    .collect(groupingBy(Dish::getType, flatMapping(dish -> dishTags.get(dish.getName()).stream(), toSet())));

groupingBy

groupingBy(x)는 사실 groupingBy(x, toList())의 축약형이다.

  • 요리의 종류를 분류하는 컬렉터로 메뉴에서 가장 높은 칼로리를 가진 요리를 찾는 프로그램
Map<Type, Optional<Dish>> mostCaloricByType = menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCaloires))));

collectingAndThen

  • 팩토리 메서드 collectingAndThen은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.
Map<Type, Dish> mostCaloricByType = 
  menu.stream()
    .collect(groupingBy(Dish::getType, collectingAndThen(maxBy(comparingInt(Dish::getCaloreis)), Optioanl::get));
  • 각 요리 형식에 존재하는 모든 CaloricLevel 값을 알고 싶은 경우(groupingBy와 mapping 사용)
Map<Type, Set<CaloricLevel>> caloricLevelByType = 
  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() )));

분할(partitioningBy)

분할은 분할 함수(partitioning function)이라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다. 분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이다.

분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다.

  • 모든 요리를 채식과 아닌 요리로 분류
Map<Boolean, List<Dish>> partitionedMenu = 
  menu.stream().collect(partitioningBy(Dish::isVegetarian));
  
List<Dish> vegetarianDishes = partitionedMenu.get(true);

List<Dish> vegetarianDishes2 = menu.stream().filter(Dish::isVegetarian).collct(toList());
  • 채식과 채식이 아닌 요리에서 가장 칼로리가 높은 음식 찾기
Map<Boolean, Dish> mostCaloricPartitioneByVegetarian =  
  menu.stream().collect(
    partitioningBy(Dish::isVegetarain, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get)));