중간 연산
- 한 스트림을 다른 스트림으로 변환하는 연산
- 여러 연산을 연결할 수 있습니다.
- 스트림 파이프라인을 구성하며, 스트림의 요소를 소비하지 않습니다.
최종 연산
- 스트림의 요소를 소비해서 최종 결과를 도출합니다.
- 스트림 파이프라인을 최적화하면서 계산 결과를 짧게 생략하기도 합니다.
👨🏻💻 컬렉터란?
- Collector 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 결정합니다.
- ex. toList, groupingBy
collect에서는 내부적으로 리듀싱 연산이 일어나는데 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리합니다.
컬렉터에서 제공하는 메서드의 기능
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
👨🏻💻 리듀싱과 요약
요소의 개수 세기
- counting
- 팩토리 메서드가 반환하는 컬렉터로 요소 수를 계산
long howManyDishes = menu.stream().count();
->
long howManyDishes = menu.stream().collect(Collectors.counting());
스트림값에서 최댓값과 최솟값 검색
- Collectors.maxBy, Collectors.minBy
- 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받습니다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
요약 연산
- Collectors.summingInt
- 객체를 int로 매핑하는 함수를 인수로 받습니다.
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
- 평균값 계산
- 두 개 이상의 연산을 한 번에 수행할 때
- summarizingInt: 요소 수, 합계, 평균, 최댓값, 최솟값 등을 한 번에 계산할 수 있습니다.
IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
문자열 연결
- joining
- 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환합니다.
- joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만듭니다. Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면 map으로 각 요리 이름을 추출하는 과정을 생략 가능
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
String shortMenu = menu.stream().collect(joining());
범용 리듀싱 요약 연산
하나의 연산을 다양한 방법으로 해결할 수 있습니다. 문제를 해결할 수 있는 다양한 방법을 확인한 다음에 가장 일반적인 문제에 특화된 해결책을 고르는 것이 바람직합니다. 생각에 또 생각을 더하자
👨🏻💻 그룹화
- 데이터 집합을 하나 이상의 특성으로 분류해서 그룹화
- 그룹화 연산의 결과로 그룹화 함수가 반환하는 키 그리고 각 키에 대응하는 스트림의 모든 항목 리스트를 값으로 갖는 맵이 반환
👨🏻💻 분할
- 분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능
- 분할 함수는 불리언을 반환하므로 맵의 키 형식은 최대 (참 아니면 거짓을 나타내는) 두 개의 그룹으로 분류
- partitioningBy
- 분할의 장점은 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다는 것이다.
- 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드된 버전을 통해 다수준으로 분할하는 기법도 존재
Collectors 클래스의 정적 팩토리 메서드 p.223 참고
👨🏻💻 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 메서드 : 새로운 결과 컨테이너 만들기
accumulator 메서드 : 결과 컨테이너에 요소 추가하기
- 리듀싱 연산을 수행하는 함수를 반환
- 함수의 반환값은 void, 즉 요소를 탐색하면서 적용하는 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기
- 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 한다.
combiner 메서드 : 두 결과 컨테이너 병합
- 스트림의 서로 다른 서브 파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의
Characteristics 메서드
- 컬렉션의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환
- 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공
- 다음 세 항목을 포함하는 열거형
- UNORDERED : 리듀싱 결과는 스트림 요소의 방문 순서나 누적 순서에 영향을 받지 않는다.
- CONCURRENT : 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있으며 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉션의 플래그에 UNORDERED를 함께 설정하지 않는다면 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.
- IDENTITY_FINISH : finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 생략 가능. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다. 또한 누적자 A를 결과 R로 안전하게 형변환할 수 있다.
👨🏻💻 커스텀 컬렉터를 구현해서 성능 개선하기
Collector 인터페이스의 각 메서드를 오버라이딩하여 구현 → 성능 향상의 결과