•
Collector 팩토리 클래스로 만든 컬렉터 인스턴스로 어떤 일을 할 수 있는지 알아보자.
•
컬렉터로 스트림의 항목을 컬렉션으로 재구성할 수 있다. 트리를 구성하는 다수준 맵, 메뉴의 칼로리 합계를 가리키는 단순한 정수 등의 형식으로 결과가 도출될 수 있다.
◦
첫 번째 예제로 couting()이라는 팩토리 메서드가 반환하는 컬렉터로 메뉴에서 요리 수를 계산한다.
long howManyDishes = menu.stream().collect(Collectors.counting());
Java
복사
1. 스트림값에서 최댓값과 최솟값 검색
•
메뉴에서 칼로리가 가장 높은 요리를 찾는다고 가정하자. Collectors.maxBy, Collectors.minBy 두 개의 메서드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.
•
두 컬렉터는 스트림의 요소를 비교하는 데 사용할 Comparator를 인수로 받는다.
•
다음은 칼로리로 요리를 비교하는 Comparator를 구현한 다음에 Collectors.maxBy로 전달하는 코드다.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream()
.collect(maxBy(dishCaloriesComparator));
Java
복사
◦
Optional<Dish>는 무슨 역할을 수행할까? 만약 menu가 비어있다면 그 어떤 값도 반환되지 않을 것이다. nullable하므로 Optional을 통해 컨테이너로 감싸준다.
2. 요약 연산
•
요약은 스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 리듀싱 연산을 주로 의미한다.
•
Collectors 클래스는 Collectors.summingInt라는 특별한 요약 팩토리 메서드를 제공한다. summingInt는 객체를 int로 매핑한 컬렉터를 반환한다.
•
summingInt가 collect 메서드로 전달되면 요약 작업을 수행한다. 다음은 메뉴 리스트의 총 칼로리를 계산하는 코드다.
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
Java
복사
•
Collectors.summingLong과 Collectors.summingDouble 메서드는 같은 방식으로 동작하며 각각 long 또는 double 형식의 데이터로 요약한다는 점만 다르다.
•
이러한 단순 합계 외에 평균값 계산 등 연산도 요약 기능으로 제공된다.
•
Collectors, averagingInt, averagingLong, averagingDouble 등 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.
double avgCalories = menu.stream().collect(averageInt(Dish::getCalories));
Java
복사
•
합계와 평균을 계산하거나 최댓값과 최솟값을 찾는 등의 연산을 한 번에 수행해야할 때도 있다. 이런 상황에서는 팩토리 메서드 summarizingInt가 반환하는 컬렉터를 사용하면 된다.
•
long이나 dobule에 대응하는 summarizingLong, summarizingDouble 등이 있다.
3. 문자열 연결
•
컬렉터에 joining 팩토리 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.
•
다음은 메뉴의 모든 요리명을 연결하는 코드다.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
Java
복사
•
joining 메서드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다.
•
Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면 다음 코드에서 보여주는 것처럼 map으로 각 요리의 이름을 추출하는 과정을 생략할 수 있다.
String shortMenu = menu.stream().collect(joining());
Java
복사
•
만약 joining()에 문자열을 인수로 전달하면 합칠 때 구분자를 넣어준다.
String shortMenu = menu.stream().collect(joining(", "));
Java
복사
4. 범용 리듀싱 요약 연산
•
지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있다. 즉 범용 Collectors.reducing으로도 정의가 가능하다는 뜻이다.
•
그럼에도 이전 예제에서 범용 팩토리 메서드 대신 특화된 컬렉터를 사용한 이유는 프로그래밍적 편의성 때문이다.
•
예를 들어 다음 코드처럼 reducing 메서드로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
Java
복사
◦
위 예제에서 reducing은 인수 세 개를 받는다.
◦
첫 번째 인수는 리듀싱 연산의 시작값이다.
◦
두 번째 인수는 요리를 칼로리 값으로 변환할 때 사용하는 함수다.
◦
세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.
•
다음처럼 한 개의 인수를 가진 reducing 버전을 이용해서 최댓값을 찾는 방법도 있다.
Optional<Dish> mostCalorieDish = menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
Java
복사
•
한 개의 인수인 reducing 팩토리 메서드는 세 개의 인수인 reducing 메서드에서 첫 번째 요소를 시작 요소, 즉 첫 번째 요소를 시작 요소로 하는데, 시작값이 없으면 값이 Nullable해진다.
•
컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
◦
reducing 컬렉터를 사용한 이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메서드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCalories, Integer::Sum));
Java
복사
▪
누적자를 초깃값으로 초기화하고 합계 함수를 이용해서 각 요소에 변환 함수를 적용한 결과 숫자를 반복적으로 조합한다.
▪
counting 컬렉터도 세 개의 인수를 갖는 reducing 팩토리 메서드를 이용해서 구현할 수 있다.
▪
다음 코드처럼 스트림의 Long 객체 형식의 요소를 1로 변환한 다음 모두 더하면 된다.
public static <T> Collector<T, ?, Long> couting() {
return reducing(0L, e -> 1L, Long::Sum);
}
Java
복사
•
위 예제에서 counting 팩토리 메서드가 반환하는 컬렉터 시그니처의 두 번째 제네릭 형식으로 와일드카드 ?이 사용되었다.
•
이 예제에서 ?는 컬렉터의 누적자 형식이 알려지지 않은 즉, 누적자의 형식이 자유로움을 의미한다. 위 예제에서는 Collectors 클래스에서 원래 정의된 메서드 시그니처를 그대로 사용했다.
◦
5장에서는 컬렉터를 사용하지 않고도 다른 방법으로 같은 연산을 수행할 수 있음을 살펴봤다.
int totalCalories = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get
Java
복사
▪
한 개의 인수를 갖는 reduce를 스트림에 적용한 다른 예제와 마찬가지로 reduce(Integer::sum)도 빈 스트림과 관련한 NULL 문제를 피할 수 있도록 Optional<Integer>를 반환한다.
▪
그리고 get으로 Optional 객체 내부의 값을 추출한다. 요리 스트림은 비어있지 않다는 사실을 우리는 알고 있으므로 get을 사용해도 상관이 없다.
▪
하지만 Optional의 값은 보통 런타임에 상황에서는 예측하기 힘드므로 가급적이면 orElse, orElseGet 등으로 구현하는 것이 좋다.
◦
마지막으로 스트림을 IntStream으로 생성하여 sum 메서드를 호출하는 방법으로도 같은 결과를 얻을 수 있다.
int totalCalories = menu.stream().mapToInt(Dish::getCalories).sum();
Java
복사
자신의 상황에 맞는 최적의 해법 선택
•
지금까지 살펴본 예제들은 함수형 프로그래밍에서 하나의 연산을 다양한 방법으로 표현할 수 있음을 보여줬다.
•
또한 스트림 인터페이스에서 직접 제공하는 메서드를 이용하는 것에 비해 컬렉터를 이용하는 코드가 더 복잡하다는 사실도 보여준다.
•
코드가 좀 더 복잡한 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화와 일반화를 얻을 수 있다.
•
문제를 해결할 수 있는 다양한 해결 방법을 알아두고서 가장 일반적으로 문제에 특화된 해결책을 고르는 것이 일반적으로 바람직하다.
•
이렇게 해서 가독성과 성능이라는 두 마리 토끼를 잡을 수 있는데, 예를 들어 메뉴의 전체 칼로리를 계산하는 예제에서는 IntStream을 사용하는 방법이 가독성이 가장 좋고 간결하다.
•
IntStream이라는 특성 덕분에 자동 언박싱 연산을 수행하거나 Integer를 int로 변환하는 과정을 생략할 수 있으므로 성능까지 좋다.