모던 자바 인 액션 - 스트림으로 데이터 수집

 

Chapter6: 스트림으로 데이터 수집

이 장에서는 다음 항목을 설명한다.
  • Collectors 클래스로 컬렉션을 만들고 사용하기
  • 하나의 값으로 데이터 스트림 리듀스하기
  • 특별한 리듀싱 요약 연산
  • 데이터 그룹화와 분할
  • 자신만의 커스텀 컬렉터 개발

collect 메서드에 Collecter 파라미터를 사용하여 원하는 연산을 간결하게 구현할 수 있다!

예제) 통화별로 트랜잭션을 그룹화한 코드
// 스트림이 아닐 때
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();

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

// 스트림 사용
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
  • 스트림을 사용할 때 훨씬 간결한 코드로 작성이 된다.

1. 컬렉터란 무엇인가?

  • 함수형 프로그래밍에서는 ‘무엇’을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경쓸 필요가 없다.
  • collect 메서드에 Collecter 인터페이스를 구현하여 전달하면 된다.
  • 명령형 코드보다 가독성과 유지보수성이 훨씬 좋다.

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

  • 훌륭하게 설계된 함수형 API의 또 다른 장점은 높은 수준의 조합성과 재사용성을 꼽을 수 있다.
  • collect로 결과를 수집하는 과정을 간단하면서도 유연한 방식으로 정의할 수 있는 점이 최대의 강점이다.
  • 스트림에 collect를 호출하면 스트림의 요소에 리듀싱 연산이 수행된다. (내부적으로 수행)
  • 보통 함수를 요소로 변환할 때는 컬렉터를 적용하며 최종 결과를 저장하는 자료구조에 값을 누적한다.
  • Collectors 유틸리티 클래스는 자주 사용되는 컬렉터 인스턴스의 정적 팩터리 메서드를 제공한다.

2) 미리 정의된 컬렉터

Collectors에서 제공하는 메서드의 기능 3가지
  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할
리듀싱과 요약 관련 기능 수행 컬렉터
  • 다양한 계산을 수행할 때 이들 컬렉터를 유용하게 활용한다.
스트림 요소를 그룹화
  • 다수준으로 그룹화하거나 각각의 결과 서브그룹에 추가로 리듀싱 연산을 적용할 수 있도록 다양한 컬렉터를 조합하는 방법을 배운다.
분할
  • 한 개의 인수를 받아 불리언으로 반환하는 함수, 즉 프레디케이트를 그룹화 함수로 사용한다.

2. 리듀싱과 요약

counting() 메서드 - 메뉴에서 요리 수를 계산
long howManyDishes = menu.stream().collect(Collectors.counting());
long howManyDishes = menu.stream().collect(counting()); // static import 시
long howManyDishes = menu.stream().count; // 단순화

1) 스트림값에서 최댓값과 최솟값을 검색

Collectors.maxBy, Collectors.minBy는 최댓값과 최솟값을 계산한다.
  • 두 컬렉터는 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
칼로리로 요리를 비교하는 Comparator
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
  • Optional은 menu가 비어있을 시 요리가 반환되지 않음을 방지한다.

2) 요약 연산

Collectors.summingInt 라는 특별한 요약 팩터리 메서드 (summingLong, summingDouble)
  • summingInt는 객체를 int로 매핑하는 함수를 인수로 받는다.
  • summingInt의 인수로 전달된함수는 객체를 int로 매핑한 컬렉터를 반환한다.
메뉴 리스트의 총 칼로리를 계산하는 코드
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
  • 칼로리로 매핑된 각 요리의 값을 탐색하면서 초깃값 0으로 설정되어 있는 누적자에 칼로리를 더한다.
Collectors.averagingInt 평균 값 계산 (averagingLong, averagingDouble)
double avgCalories = menu.stream().collect(averagingInt(Dish::getCalores));
Collectors.summarizingInt 두 개 이상의 연산을 한 번에 수행한다.
IntSummaryStatistics = menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
  • IntSummaryStatistics 클래스로 모든 정보가 수집된다.

3) 문자열 연결

Collectors.joining 팩터리 메서드

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

메뉴의 모든 요리명을 연결하는 코드
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
  • joining 메서드는 내부적으로 StringBuilder를 이용해 문자열을 하나로 만든다.
연결된 요소 사이에 구분 문자열을 넣는 joining 메서드
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));

4) 범용 리듀싱 요약 연산