컬렉터란 무엇인가?
컬렉터 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.
리듀싱과 요약
컬렉터로 스트림의 모든 요소를 하나의 결과로 합칠 수 있다.
스트림값에서 최대값 최소값 검색
Collectors.maxBy()
- 스트림의 최대값 계산
- 스트림 요소를 비교하는데 Comparator를 인수로 받음
Collectors.minBy()
- 스트림의 최소값 계산
- 스트림 요소를 비교하는데 Comparator를 인수로 받음
요약 연산
스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산
Collectors.summingInt()
- 객체를 int로 매핑하는 함수를 인수로 받음
- int로 매핑된 요소의 총합 계산
- summingLong(), summingDouble도 존재
Collectors.averagingInt()
- 객체를 int로 매핑하는 함수를 인수로 받음
- int로 매핑된 요소의 평균 계산
- averagingLong(), averagingDouble도 존재
Collectors.summarizingInt()
- 하나의 요약 연산으로 수, 합계, 평균, 최댓값, 최소값을 구할 수 있음
- IntSummaryStatistics 클래스로 return
- summarizingLong, summarizingDouble도 존재
문자열 연결
joining()
- 스트림의 각 객체에 toString 메서드를 호출하여 추출한 모든 문자열을 하나의 문자열로 반환
범용 리듀싱 요약 연산
이전까지의 컬렉터는 reducing 메서드를 통해서도 정의할 수 있다.
reducing()
- 첫 번째 인수는 리듀싱 연산의 시작 값이거나 스트림에 인수가 없는 경우 반환값
- 두 번째 인수는 변환 함수
- 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator
- 한 개의 인수만 넘겨준다면 BinaryOperator를 넘겨야함
그룹화
데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 작업
Collectors.groupingBy()
- 스트림의 요소들을 인수로 넘기는 함수를 기준으로 그룹화하며 이 함수를 분류 함수라고 함
- 그룹화 연산의 결과로 그룹화 함수가 반환하는 키와 각 키에 대응하는 스트림의 모든 항목 리스트를 값으로 갖는 Map반환
List<Book> books = asList(new Book("2022신상책", "홍길동", 2022, "IT"),
new Book("이순신의 바다", "황현필", 2021, "History"),
new Book("갈매기의 꿈", "리처드 바크", 2020, "Fiction"),
new Book("모던 자바 인 액션", "라울-게이브리얼 우르마", 2020, "IT"),
new Book("2019년 책", "홍길동", 2019, "IT"),
new Book("이펙티브 자바", "조슈아 블로크", 2018, "IT")
);
Map<String, List<Book>> collect = books.stream().collect(Collectors.groupingBy(Book::getField));
결과
{IT=[Book{name='2022신상책', author='홍길동', year=2022, field='IT'}, Book{name='모던 자바 인 액션', author='라울-게이브리얼 우르마', year=2020, field='IT'}, Book{name='2019년 책', author='홍길동', year=2019, field='IT'}, Book{name='이펙티브 자바', author='조슈아 블로크', year=2018, field='IT'}], History=[Book{name='이순신의 바다', author='황현필', year=2021, field='History'}], Fiction=[Book{name='갈매기의 꿈', author='리처드 바크', year=2020, field='Fiction'}]}
그룹화된 요소 조작
요소를 그룹화한 후에 각 결과 그룹의 요소를 조작하는 연산이 필요함.
그룹화 전에 predicate로 필터를 적용해서 해결할 수도 있지만 이 경우 만족하는 결과가 없으면 키 자체가 사라진다.
groupingBy의 두 번째 인수로 필터 predicate를 줘서 이를 해결할 수 있다.
Map<String, List<Book>> collect2 = books.stream()
.collect(Collectors.groupingBy(Book::getField, Collectors.filtering(book -> book.getYear() >= 2021, Collectors.toList())));
{IT=[Book{name='2022신상책', author='홍길동', year=2022, field='IT'}], History=[Book{name='이순신의 바다', author='황현필', year=2021, field='History'}], Fiction=[]}
mapping()
- 매핑함수와 각 항목의 적용한 함수를 모으는데 사용하는 메서드
Map<String, List<String>> collect3 = books.stream()
.collect(Collectors.groupingBy(Book::getField, Collectors.mapping(Book::getName, Collectors.toList())));
다수준 그룹화
두 인수를 받는 groupingBy를 사용하여 항목을 다수준으로 그룹화 할 수 있다.
Map<String, Map<String, List<Book>>> collect4 = books.stream()
.collect(Collectors.groupingBy(Book::getField, Collectors.groupingBy(book -> {
if (book.getYear() < 2020) {
return "old book";
} else {
return "new book";
}
})));
{
IT={
new book=[
Book{name='2022신상책', author='홍길동', year=2022, field='IT'},
Book{name='모던 자바 인 액션', author='라울-게이브리얼 우르마', year=2020, field='IT'}],
old book=[
Book{name='2019년 책', author='홍길동', year=2019, field='IT'},
Book{name='이펙티브 자바', author='조슈아 블로크', year=2018, field='IT'}
]
},
History={
new book=[
Book{name='이순신의 바다', author='황현필', year=2021, field='History'}
]
},
Fiction={
new book=[
Book{name='갈매기의 꿈', author='리처드 바크', year=2020, field='Fiction'}
]
}
}
분할
분할 함수라 불리는 predicate를 분류함수로 사용하는 특수한 그룹화 기능
Boolean을 반환하여 그룹화는 True인 그룹과 false인 그룹으로 분류된다.
partitioningBy()
- 인수로 분할함수를 넘겨주면 true, false 두그룹으로 분류한 map을 반환
Map<Boolean, List<Book>> collect5 = books.stream()
.collect(Collectors.partitioningBy(book -> book.getYear() < 2020));
Collector 인터페이스
리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합
public interface Collector<T, A, R>{
Supplier<A> supplier();
BiConsumer<A,T> accumulator();
BinaryOperator<A> combiner();
Function<A,R> finisher();
Set<Characteristics> characteristics();
}
- T: 수집될 스트림 항목의 제너릭 형식
- A: 누적자. 수집 과정에서 중간 결과를 누적하는 객체의 형식
- R: 수집 연산 결과 객체의 형식
supplier() - 새로운 결과 컨테이너 만들기
- 빈 결과로 이루어진 supplier를 반환해야 함
- 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
accumulator() - 결과 컨테이너에 요소 추가하기
- 리듀싱 연산을 수행하는 함수 반환
- n번째 요소 탐색시 누적자(n-1번쨰 요소까지 연산한 상태)와 n번쨰 요소를 함수에 적용
- 반환값은 void
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
finisher() - 최종 변환값을 결과 컨테이너로 적용하기
- 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 함
- 반환값은 void
- 누적자 객체가 이미 최종 결과인 경우 항등함수를 반환
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
combiner() - 두 결과 컨테이너 병합
- 리듀싱 연산에서 사용할 함수 반환
- 서로 다른 서브 파트를 병렬로 처리할 때 누적자가 결과를 어떻게 처리할지 정의
- 스트림의 두 번째 서브파트에서 수합한 리스트를 첫번째 서브파트 결과 리스트에 추가하면 됨
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
Characteristics()
- 컬렉터의 연산을 정의하는 Characteristics형식으로 불변 집합을 반환
- 스트림을 병렬로 리듀스할 것인지, 만약 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공
- 아래 세 항목을 포함하는 열거형
- UNOORDERED
- 리듀싱 결과는 스트림의 요소의 방문 순서나 누적 순서에 영향을 받지 않음.
- CONCURRENT
- 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있음.
- 컬렉터의 플래그에 UNORDERED를 설정하지 않았다면 데이터 소스가 정렬되지 않은 상황에서만 병렬 리듀싱을 수행할 수 있음.
- IDENTITY_FINISH
- finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 생략 가능.
- 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있음.
- UNOORDERED
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}
참고문서
모던 자바 인 액션 - 라울 게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로소포트
'프로그래밍 > Book' 카테고리의 다른 글
[모던 자바 인 액션] 8장. 컬렉션 API 개선 (0) | 2022.02.21 |
---|---|
[모던 자바 인 액션] 7장. 병렬 데이터 처리와 성능 (0) | 2022.02.21 |
[모던 자바 인 액션] 5장. 스트림 활용 (0) | 2022.02.06 |
[모던 자바 인 액션] 4장. 스트림 소개 (0) | 2022.01.31 |
[모던 자바 인 액션] 3장. 람다 표현식 (0) | 2022.01.28 |