06. 스트림으로 데이터 수집

간단 정리#

  • collector는 스트림 요소를 요약 결과로 누적하는 다양한 방법을 인수로 갖는 최종 연산
  • 스트림 요소를 하나의 값으로 리듀스하고 요약하는 다양한 컬렉터가 미리 정의되어 있음
  • 스트림 요소 그룹화: groupingBy()
  • 스트림 요소 분할: partitioningBy()
  • 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있음
  • 커스텀 컬렉터를 Collector 인터페이스를 구현함으로 구현 가능

Collectors 클래스로 컬렉션 만들고 사용#

Collector 란#

  • Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다
Map<Currency, List<Transaction>> transactionsByCurrencies =
transactions.stream()
.collect(groupingBy(Transaction::getCurrency));

Collector 특징#

  • 가독성과 유지보수성을 증가시킬 수 있음
  • collect 를 호출하면 리듀싱 연산이 일어남

미리 정리된 컬렉터#

  • Collectors 에 미리 리듀싱을 위해 팩터리 메서드들이 정리되어 있음
  • Collectors 기능 세 가지
    • 요약: 스트림 요소를 하나의 값으로 리듀스
    • 요소 그룹화
    • 요소 분할

요약: 하나의 값으로 데이터 스트림 리듀스하기#

정적 팩터리 메서드#

  • counting()
  • minBy() maxBy()
  • summingInt() averagingInt() summarizingInt()
    • 기본 타입인 int, long, double 지원
  • joining()
    • 문자열 하나로 합치기

특별한 리듀싱 요약 연산#

  • reducing()
    • 파라미터로 초깃값, 변환 함수, 합계 함수를 받음
    • 파라미터가 하나로 구성될 땐 합계 함수만 사용됨
      • 초깃값은 원소 첫번째
      • 변환함수는 항등함수 (e -> e)

데이터 그룹화와 분할#

그룹화: 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 연산

그룹화#

groupingBy()#

아래의 예제에서 Dish::getType 과 같이 분류 기준이되는 함수를 분류함수 라 함

일반적인 방식
Map<Dish.Type, List<Dish>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));

groupingBy() 의 두번째 인자로 filtering mapping flatMapping 등 분류화 결과 값에 대한 람다를 활용할 수 있음

Collector 형식의 두번째 인자 활용
// Dish의 Type에 대한 모든 키가 포함되도록 구성 (빈 리스트도)
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));

다수준 그룹화#

중첩 맵을 만들어야할 때 활용 가능

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;
}))
);

분할#

  • 분할 함수는 Predicate 를 활용함
  • 두번째 인자는 groupingBy 와 같이 분할할 대상의 결과 값을 추출할 람다를 활용할 수 있음
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegetarian));
List<Dish> vegetarianDishes = partitionedMenu.get(true);

분할 장점#

  • 참, 거짓에 대한 스트림 리스트를 모두 유지함

자신만의 커스텀 컬렉터 개발#

Collector 인터페이스를 구현하여 자신만의 커스텀 컬렉터를 개발할 수 있음

Collector 인터페이스#

리듀싱 연산에 대한 함수 정의들로 이루어짐

Collector 인터페이스
// 정의
public interface Collector<T, A, R> {
// 새로운 결과 컨테이너
Supplier<A> supplier();
// 결과 컨테이너 요소 추가
BiConsumer<A, T> accumulator();
// 최종 변환값
Function<A, R> finisher();
// 병렬 처리 시 다른 서브 스트림을 어떻게 병합시킬 지
BinaryOperator<A> combiner();
// 해당 컬렉터의 특징 enum들의 set을 반환
Set<Characteristics> chracteristics();
}
// 예시
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>;
return ArrayList::new; // 메서드 레퍼런스도 가능 (여기선 생성자)
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
return List::add; // 메서드 레퍼런스 활용 시
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(
EnumSet.of(UNORDERED, CONCURRENT, IDENTITY_FINISH)
);
}
}
  • 타입 설명
    • T: 수집될 스트림 항목의 제네릭 형식
    • A: 누적자, 수집 과정에 중간 결과를 저장할 객체 형식
    • R: 수집 연산 결과에 대한 객체 형식
  • Characteristics 설명
    • UNORDERED: 리듀싱 결과는 입력 요소 순서를 보장하지 않음
    • CONCURRENT: 다중 스레드 가능
    • IDENTITY_FINISH: 정의되어 있을시 finisher는 항등 함수, 즉 Function.identity() 를 리턴하도록 구성되어 있음을 알림
Last updated on