스트림으로 데이터 수집
컬렉터
Collector 인터페이스는 어떻게 구현하느냐에 따라 스트림에 어떤 리듀싱 연산을 수행할지 결정된다. Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드를 제공한다.
Collectors에서 제공하는 메서드의 기능은 크게 세가지로 구분할 수 있다.
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
리듀싱과 요약
- counting: 개수를 카운트한다
- maxBy, minBy: 최대 혹은 최소를 만족하는 요소를 찾는다
- summingInt: 객체를 int로 매핑하는 인수를 받아 합 계산
- averagingInt: 객체를 int로 매핑하는 인수를 받아 평균을 계산한다
- summarizingInt: 요소 수, 합계, 평균, 최대값, 최소값 등을 계산한다.
- joining: 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.
collect와 reduce
collect와 reduce는 둘다 최종 연산 즉, stream API에서 데이터를 처리하는데 사용되는 메서드로 유사한 기능을 수행할 수 있지만 의미적, 실용적 차이가 있다.
collect는 가변 컨테이너에 데이터를 수집한다. List, Map, Set과 같은 자료구조에 데이터를 저장할 때 주로 사용된다. 가변 컨테이너를 사용하기 때문에 병렬 스트림에서도 안전하게 사용할 수 있다.
reduce는 불변성을 유지하며 두 값을 하나로 병합하는 연산을 반복적으로 수행한다. 합계, 곱샘, 최대값, 최소값 등 누적 연산을 수행할 때 사용된다.
collect는 결과를 누적할 때 컨테이너를 사용하므로 상태를 가진다. 반면 reduce는 상태를 가지지 않고 최종 값만 반환한다.
여러 스레드가 동시에 데이터 구조체를 고치면 컬렉션이 망가지므로 리듀싱 연산을 병렬로 수행할 수 없다. 이럴 때 가변 컨테이너 관련 작업이면서 병렬성을 확보하려면 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직하다.
그룹화
그룹화 함수는 스트림을 분류하는 속성을 지녔기에 분류 함수라고 부른다.
- groupingBy: 그룹핑에 핵심적인 메서드이며 많은 오버로딩된 메서드를 가진다.
groupingBy -> Map<K, List> 형태의 결과 반환- K: 그룹화 기준이 되는 키
- List<T>: 해당 키에 해당하는 원소들의 리스트
컬렉터 중첩
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::Type, // 분류 함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get)); // 변환 함수
컬렉터를 중첩할 시 다음과 같은 작업이 수행된다.
- groupingBy에서 분류하는 요소(Dish.Type)별로 그룹화한다. 예를 들어, Dish.Type이 MEAT, FISH, OTHER라면, 각 타입별로 음식들을 나눈다.
- collectingAndThen는 그룹화된 데이터에 추가적인 변환 작업을 수행한다. 첫번째 파라미터인 maxBy의 실행 결과를 받아 변환 함수(Optional::get)의 결과로 변환한다.
- maxBy는 그룹화된 각 타입에서 가장 비교 값이 높은 Dish를 선택한다.
- maxBy는 Optional을 반환하므로 Optional::get을 통해 실제 값을 꺼낸다.
분할
partitioningBy는 특정 조건(Predicate)에 따라 데이터를 두 그룹으로 분할하는 그룹화 메서드이다. 결과적으로 True 그룹과 False 그룹으로 나뉜 Map<Boolean, List<T>> 형태의 데이터를 반환한다.
- partitioningBy(Predicate<? super T> predicate) -> Map<Boolean, List>
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> partitioned = numbers.stream()
.collect(Collectors.partitioningBy(num -> num % 2 == 0));
// false = [1, 3, 5], true = [2, 4, 6]
Collector 인터페이스
리듀싱 연산을 사용자 정의 방식으로 구현하기 위한 메서드 집합.
이를 통해 기본 제공 컬렉터(Collectors.toList(), Collectors.groupingBy() 등) 대신 맞춤형 컬렉터를 만들 수 있다.
- Collector<T, A, R>의 구성
- T: 스트림에서 처리하는 입력 요소 타입
- A: 리듀싱 작업 중 중간 결과를 저장할 누적기 타입
- R: 최종 결과 타입
커스텀 컬렉터 만들기 예시
public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new; // 새로운 리스트 생성
}
@Override
public BiConsumer<List<T>, T> accumulator() {
return List::add; // 리스트에 요소 추가
}
@Override
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2); // 두 리스트 병합
return list1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return Function.identity(); // 리스트 자체를 반환
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
}
}
사용 방법
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
List<Integer> result = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수 필터
.collect(new ToListCollector<>()); // 커스텀 컬렉터 사용
System.out.println(result); // [2, 4]
'Java' 카테고리의 다른 글
모던 자바 인 액션 - chapter 7, 8 병렬 데이터 처리와 성능, 컬렉션 API 개선 (0) | 2024.11.30 |
---|---|
모던 자바 인 액션 - chapter 4, 5 스트림과 활용 (1) | 2024.11.20 |
모던 자바 인 액션 - chapter 2, 3 동작 파라미터화, 람다 표현식 (1) | 2024.11.19 |
모던 자바 인 액션 - chapter 1 자바 8 무슨 일이 일어나고 있는가 (0) | 2024.11.19 |
스트림(2) - Stream API의 다양한 연산 (0) | 2024.10.26 |