중간연산은 스트림 파이프라인을 구성하며 스트림의 요소를 소비하지 않음.
반면 최종연산은 스트림의 요소를 소비해서 최종결과를 도출.
이전 챕터의 예제에서는 toList 로 스트림 요소를 리스트로만 변환했지만
이번 챕터에서는 reduce가 그랬던것처럼 collect 역시 다양한 요고 누적방식을 인수로 받아
스트림을 최종결과로 도출하는 리듀싱 연산을 수행할 수 있음을 설명한다.
다양한 요소 누적 방식은 Collector 인터페이스에 정의되어 있다.
Collection, Collector, collect를 헷갈리지 않게 주의하자.
collect와 컬렉터로 구현할 수 있는 질의 예제
- 통화별로 트랜젝션을 그룹화한 다음에 해당 통화로 일어난 모든 트렌잭션 합계를 계산 (Map<Currency, Integer> 반환)
- 트렌잭션을 비싼 트랜잭션과 저렴한 트랜젝션 두 그룹으로 분류 (Map<Boolean, List<Transaction>> 반환)
- 트랜잭션을 도시 등 다수준으로 그룹화. 그리고 각 트랜잭션이 비싼지 저렴한지 구분 ( Map<String, Map<Boolean, List<transaction>>>반환)
통화별로 트랜잭션을 그룹화한 코드
// (명령형 버전)
// 그룹화한 트랜잭션을 저장할 맵 생성.
Map<Currency, List<Transaction>> transactionsByCurrencies = new HashMap<>();
for(Transaction transaction : transactions) {
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrency.get(currency);
if(transactionsForCurrency == null) {
transactionsForCurrency = new ArrayList<>(); //맵에 항목 없으면 만듦
transactionsForCurrency.put(currency, transactionsForCurrency);
}
transactionsForCurrency.add(transaction);
}
// (스트림버전)
Map <Currency, List<Transaction>> transactionsForCurrency =
transactions.stream().collect(groupingBy(Transaction::getCurrency));
스트림에 toList 를 사용하는 대신 더 범용적인 컬렉터 파라미터를 collect 메서드에 전달함으로써 원하는 연산을 간결하게 구현
6.1 컬렉터란 무엇인가?
함수형 프로그래민에서는 '무엇'을 원하는지 명시할 수 있어 어떤 방법으로 얻을지 신경 쓸 필요가 없다.
이전 예제에서는 collect 메서드로 Collector 인터페이스 구현을 전달.
여기서는 groupingBy를 이용해 '각 키 버킷 그리고 각 키 버킷에 대응하는 요소 리스트를 값으로 포함하는 맵을 만들라' 는 동작 수행.
1 - 고급 리듀싱 기능을 수행하는 컬렉터
collect로 결과를 수집하는 과정을 간단하면서도 유영한 방식으로 정의할 수있는 점이 컬렉터의 최대 장점
스트림에 collect 를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행.
Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적 팩토리 메서드 제공
toList는 스트림의 모든 요소를 리스트로 수집
List<Transaction> transactions = transactionStream.collect(Collectors.toList());
2 - 미리 정의된 컬렉터
Collectors에서 제공하는 메서드의 기능
- 스트림 요소를 하나의 값으로 리듀스하고 요약
- 요소 그룹화
- 요소 분할
6.2 리듀싱과 요약
트리를 구성하는 다수준 맵, 메뉴의 칼로리 합계를 가리키는 단순한 정수 등 다양한 형식으로 결과를 도출 가능.
counting 컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.
import static java.util.stream.Collectors.*;
정적팩토리메서드를 모두 임포트 했다고 가정하면
Collectors.counting()을 간단하게 counting()으로 표현 가능.
1 - 스트림 값에서 최대값과 최소값 검색
Collectors.maxBy, Collectors.minBy 두개의 메서드 이용해 계산 .
스트림 요소를 비교하는데 사용할 Comparator를 인수로 받는다.
요리를 비교하는 Comparator를 구현한 다음에 Collectors.maxBy로 전달하는 코드.
Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(dishCaloriesComparator));
Optional<Dish> 는 만약 menu가 비어있다면 어떤 요리도 반환되지 않음.
객체 숫자필드의 합계나 평균을 반환하는 연산에도 리듀싱 기능히 자주 사용 - 요약 연산.
2 - 요약 연산
summingInt는 객에를 int로 매핑하는 함수를 인수로 받음.
인수로 전달된 함수는 객체를 int 로 매핑한 컬렉터를 반환.
그리고 summingInt가 collect 메서드로 전달되면 요약작업 수행
리스트의 총 칼로리 계산하는 코드
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
두개 이상의 연산을 한번에 수행해야 할때
팩토리메서드 summarizingInt가 반환하는 컬렉터를 사용
IntSummaryStatistics menuStatistics = menu.stream().collect(summingInt(Dish::getCalories));
IntSummaryStatistics클래스로 모든 정보가 수집된다. 출력해보면 다음과 같은 정보를 확인 가능
IntSummaryStatistics{count=9, sum=4300, min=120, average=477.77778, max=800}
3 - 문자열 연결
컬렉터에 joinging 팩토리 메서드를 이용하면 스트림의 각 객체에 toString메서드를 호출해
추출한 모든 문자열을 하나의 문자열로 연결해서 반환.
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
joinging 메서드는 내부적으로 StringBuilder를 이요해 문자열을 만들기때문에
Dish 클래스가 요리명을 반환하는 toString 메서드를 포함하고 있다면 과정 생략 가능.
String shortMenu = menu.stream().map().collect(joining());
요소사이 구분 문자열을 넣을수도 있음.
String shortMenu = menu.stream().map(Dish::getName).collect(joining(" ,"));
4 - 범용 리듀싱 요약 연산
지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로 정의할 수도 있으나
특화된 컬렉터를 사용하는 이유는 프로그래밍 편의성, 가독성 때문이다.
다음과 같이 모든 칼로리를 계산 가능하다.
세개의 인수
int totalCalories = menu.stream().collect(reducing(0, Dish::getCalories, (i,j) -> i+j ));
- 첫번째 인수를 시작값이거나, 스트림에 인수가 없을때 반환값이다.
- 두번째 인수는 변환함수다.
- 세번째 인수는 값은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator다.
가장 칼로리가 높은 요리 찾는법
한개의 인수
Optional<Dish> mostColorieDish = menu.stream()
.collect(reducing((d1, d2) -> d1.get(Calories() > d2.getColories() ? d1 :d2));
한개의 인수를 같는 reducing은 Oprional<Dish> 객체를 반환한다.
컬렉션 프레임워크 유연성 : 같은 연산도 다양한 방식으로 수행할 수 있다.
람다표현식 대신 Integer클래스의 sum 에서드 참조를 이용해 코드를 단순화
int totalCalories = menu.stream().collect(
reducing(0, // 초기값
Dish::getCaloties, // 합계함수
Integer::sum)); // 변환함수
스트림을 IntStream으로 매핑한 후 sum 메서드 호출하는 방법도 같은 결과를 얻을수 있다.
int totalColories = menu.stream().mapToInt(Dish::getCalories).sum();
자신의 상황에 맞는 최적의 해법 선택
함수형 프로그래밍에서는 하나의 연산을 다양한 방법으로 해결 가능
스트림 임터페이스에서 직접 제공하는 메서드를 이용하는 것에비해 컬렉터를 이용하는 코드가 더 복잡함.
코드가 복잡하나, 대신 재사용성과 커스터마이즈 가능성을 제공하는 높은 수준의 추상화를 얻을수 있음.
전체 칼로리를 계산하는 데에는 IntStream을 활용한 방법이 가독성이 좋고 간결함.
자동 언박싱, int 변환과정도 피할수 있으므로 성능도 좋음.
6.3 그룹화
Collectors.groupingBy를 이용해 쉽게 메뉴를 그룹화
Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::getType));
분류함수로 그룹화된 결과
{FISH=[prawn, salmon], OTHER=[french fries, rice, season fruit, pizza], MEAT=[pork, chicken, beef]}
속성 접근자 대신 분류기준이 핑요한 상황.
메서드 참조 대신 람다 표현식으로 필요한 로직 구현
public enum CaloricLevel {DIET, NORMAL, FAT}
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(
groupingBy(dish -> {
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700) return CaloricLevel.NOMAL;
else return CaloricLevel.FAT;
})
);
두가지 기준으로 동시에 그룹화 할 수 있을까?
1 - 그룹화된 요소 조작
다음 코드처럼 그룹화 하기전에 프레디케이트로 필터를 적용해 문제 해결 가능할까?
Map<Dish.Type, List<Dish>> caloricDishByType = menu.stream()
.filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
// 결과
// 타입중 메뉴에 없는경우 타입이 맵키에 들어가지 않음.
{OTHER=[french fries, pizza], MEAT=[pork, beef]}
두번째 Collectors 안에 필터 프레디케이트를 이동함으로 문제 해결
Map<Dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getColories() > 500, toList)));
// 결과
// 비어있는 FISH도 항목으로 추가
{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
그룹화된 항목을 조작하는 유용한 기능중 하나로 매핑함수를 이용해 요소를 변환하는 작업
또 다른 컬렉터를 인수로 받는 mapping 메서드를 제공
이 함수를 이용해 그룹의 각 요리를 관련 이름 목록으로 변환 가능
Map<Dish.Typ, List<String>> dishNamsByType = menu.stream()
.collect(groupingBy(Dish::getType, mapping(Dish::getName, toList())));
Map<String, List<String>> dishTags = new HashMap<>();
flatMapping 컬렉터를 이용하면 각 형식의 요리의 태그를 간편하게 추출가능
Map<String, List<String>> dishTags = new HashMap<>();
dishTags.put("pork", asList("greasy", "salty"));
dishTags.put("beaf",asList("salty","rosted"));
dishTags.put("chicken",asList("fried","crispy"));
dishTags.put("french fries",asList("greasy","fried"));
...
Map<Dish.Type, Set<String>> dishNameByType = menu.stream()
.collect(groupingBy(Dish::getType,
flatMapping(dish-> dishTags.get(dishName().stream(),
toSet()))));
// flatMapping 연산결과를 수집해서 리스트가 아닌 집합으로 그룹화해 중복 태그를 제거
{MEAT=[salty, greasy, rosted, fried, crisp], Fish= ... }
2 - 다수준 그룹화
groupingBy 를 이용해서 항목을 다수준으로 그룹화 가능
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;
})
)
);
//두 수준의 맵이 만들어짐
{MEAT={DIET=[chiecken], NORMAL=[beef], FAT=[pork]}, FISH={DIET=[prowns],
NORMAL=[salmon]}, {OTHER={DIET=[rice, seasonal fruit], NORMAL=[french fries, pizza]}}
연산을 버킷 개념으로 생각하면 쉽다.
각 키의 버킷을 만들고 준비된 버킷을 스트림 컬렉터로 채워가며 반복해 n수준 그룹화 달성.
칼로리 \ 종류 | FISH | MEAT | OTHER |
DIET | prawn | chicken | fruit, rice |
NOMAL | salmon | beef | pizza, fries |
FAT | pork |
3 - 서브그룹으로 데이터 수집
첫번째 groupingBy 로 넘겨주는 컬렉터의 형식은 제한이 없다.
groupingBy 컬렉터에 두번째 인수로 counting 컬렉터를 전달해 메뉴에서 요리의 수를 종류별로 계산 가능.
Map<Dish.Type, Long> typesCount = menu.stream().collect( groupingBy(Dish::getType, counting()));
컬렉터 결과를 다른 형식에 적용하기
팩토리메서드 Collectors.collectingAndThen으로 컬렉터가 반환한 결과를 다른형식으로 활용 가능
// 각 서브그룸에서 가장 칼로리가 높은 요리 찾기
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType, // 분류함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get)) // 변환함수
);
// 결과
{FISH=salmon, OTHER=pizza, MEAT=pork}
-> 컬렉터 groupingBy 가장 바깥쪽에 위치하면서 요리의 종류에 따라 메뉴 스트림을 세개의 서브스트림으로 그룹화
-> groupingBy 컬렉터는 collectingAndThen 컬렉터를 감싼다.
-> 두번째 컬렉터는 그룹화된 세개의 서브스트림에 적용
-> collectingAndThen 컬렉터는 세번째 컬렉터 maxBy를 감싼다.
-> 리듀싱 컬렉터가 서브스트림에 연산을 수행한 결과 collectingAndThen의 Optional::get 변환함수가 적용
-> groupingBy 컬렉터가 반환하는 맵의 분류키에 대응하는 세 값이 각각 요리 형식의 가장 높은 칼로리.
groupingBy 와 함께 사용하는 다른 컬렉터 예제
// groupingBy에 두번째 인수로 전달한 컬렉터를 각 그룹으로 분류된 요리에 사용
Map<Dish.Type, Integer> totalCaloriesByType =
menu.stream().collect(groupingBy(Dish::getType,
summingInt(Dish::getCalories)));
// mapping 메서드로 만들어진 컬렉터도 자주 사용됨.
//각 요리 형식에 존재하는 모든 CaloricLevel값
menu.stream().collect(
groupingBy(Dish::getType, mappring(dish-> {
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;},
toSet() )
));
// toCollection을 이용하면 원하는 방식으로 결과를 제어가능
// 메서드참조 HashSet::new 를 toCollection에 전달
Map<Dish.Type, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(dish->{
if(dish.getCalories() <= 400) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else retun CaloricLevel.FAT;},
toCollection(HashSet::new) )));
6.4 분할
분할 함수 partitioning function이라 불리는 프레디케이트를 분류함수로 사용하는 특수한 그룹화기능.
불리언을 반환하므로 맵의 키 형식을 Boolean
결과적으로 그룹화 맵은 최대 두개의 그룹으로 분류됨.
Map<Boolean, List<Dish>> partitionedMenu =
menu.stream().collect(partitioningBy(Dish::isVegeterian)); //분할함수
//결과
{false=[pork, beef, chiken, prawns, salmon],
true=[french fries, rice, season fruit, pizza]}
// 참값의 키로 맵에서 모든 채식요리를 얻을수 있음.
List<Dish> vegetarianDishes = partitionedMenu.get(true);
// 메뉴리스트 스트림을 프레디케이트로 필터링 후 리스트에 수집해도 같은 결과
List<Dish> vegetarianDishes =
menu.stream().filter(Dish::isVegetarian).collect(toList());
1 - 분할의 장점
partitioningBy가 반환하나 맵 구현은 참과 거짓 두 가지 키만 포함하므로 간결하고 효과적
// 채식이 아닌 모든 요리 리스트
// 컬렉터를 두번째 인수로 전달 할 수 있는 오버로드 버전의 partitioningBy 메서드
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType =
menu.stream().collect(
patitioningBy(Dish::isVegeterian, //분할함수
groupingBy(Dish::getType))); //두번째 컬렉터
//결과
{false={FISH=[prawns, salmon], MEAT=[pork, beef, chicken]},
{true={OTHER=[french fries, rice, season fruit, pizza]}}
//채식요리와 채식이 아닌 요리 그룹의 가장 칼로리가 높은 요리
Map<Boolean, Dish> mostCaloricPartitionedByVegetarian =
menu.stream().collect(
partitioningBy(Dish::isVegetarian,
collectingAndThen(maxBy(comparingInt(Dish::getCalories)),
Oprional::get)));
//결과
{false=pork, true=pizza}
2 - 숫자를 소수와 비소수로 분할하기
정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누는 프로그램 구현.
// 먼저 주어진 수가 소수인지 아닌지 판단하는 프레디케이트를 구현
public boolean isPrime(int candidate){
return IntStream.range(2, candidate)
.noneMatch(i-> candidate % i == 0);
}
//소수의 대상을 주어진 수의 제곱근 이하의 수로 제한
public boolean isPrime(int candidate){
int candidateRoot = (int)Math.sqrt((double)candidate);
return IntStream.rangeClosed(2, candidateRoot)
.noneMatch(i-> candidate % i == 0);
}
//isPrime 메서드를 프레디케이트로 이용하고 partitioningBy 컬렉터로 리듀스
//숫자를 소수와 비소수로 분류
public Map<Boolean, List<Integer>> partitionPrimes(int n) {
return IntStream.rangeClosed(2,n).boxed()
.collet(
partitioningBy(candidate->isPrime(candidate)));
}
6.5 Collector 인터페이스
toList 가 어떻게 구현되었는지 살펴보면서 Collector는 어떻게 정의 되어있고,
내부적으로 collect 메서드는 toList가 반환하는 함수를 어떻게 활용했는지 살펴보자.
//Collector 인터페이스
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, T> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
- T는 수집될 스트림 항목의 제네릭 형식이다.
- A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
- R은 수집 연산 결과 객체의 형식(대개 컬렉션 형식)이다.
Stream<T> 의 모든 요소를 List<T>로 수집하는 ToListCollector<T>라는 클래스 구현
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
누적과정에서 사용되는 객체가 수집과정의 최종결과로 사용된다.
1 - Collector 인터페이스의 메서드 살펴보기
Collector의 정의된 다섯개의 메서드중
네개의 메서드는 collect 메서드에서 실행하는 함수를 반환하는 반면,
다섯번째 메서드는 characteristics는 collect 메서드가 어떤 최적화를 이용해
리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성집합을 제공
supplier 메서드: 새로운 결과 컨테이너 만들기
빈 결과로 이루어진 Supplier 반환
수집과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수.
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
//생성자 참조를 전달하는 방법
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
accumulator 메서드: 결과 컨테이너에 요소 추가하기
리듀싱 연산을 수행하는 함수를 반환한다.
ToListCollector에서 accumulator 가 반환하는 함수는
이미 탐색한 항목을 포함하는 리스트에 현재 항목을 추가하는 연산을 수행.
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
//메서드 참조 이용
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
finisher 메서드: 최종 변환값을 결과 컨테이너로 적용하기
스트림 탐색을 끝내고 누적자 객체를 최종결과로 변환하면서
누적과정을 끝낼때 호출할 함수를 반환
// 변환과정이 필요하지 않으므로 finisher 메서드는 항등함수를 반환
public Function<List<T>, List<T>> finisher() {
reutn Funcion.identity();
}
combiner 메서드: 두 결과 컨테이터 병합
스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이결과를 어떻게 처리할지 정의
// 스트림의 두번째 서브파트에서 수집한 항목 리스트를 첫번째 서브파트 결과 리스트에 추가.
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
characteristics 메서드
컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환.
어떤 최적화를 선택해야 할지 힌트를 제공.
다음 세 항목을 포함하는 열거형이다.
- UNORDERED: 리듀싱 결과는 스트림 요소의 방문 순서나 누적순서에 영향을 받지 않음
- CONCURRENT: 다중 스레드에서 accumulator 함수를 동시에 호출 가능. 스트림의 병렬 리듀싱 수행 가능. 컬렉터 플래그에서 UNORDERED를 함께 성정하지 않으며 데이터소스가 정렬되지않은 상황에서만 병렬 리듀싱 수행가능.
- IDENTITY_FINISH: 단순히 identity를 적용할 뿐이므로 생략가능. 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용 가능. 또한 누적자 A를 결과 R로 안전하게 형변환 가능.
2 - 응용하기 : ToListCollector 구현
import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;
import static java.util.stream.Collector.Characteristics.*;
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 Function<List<T>, List<T>> finisher() {
return Function.identity(); // 항등함수
}
@Override
public BineryOperator<List<T>> combiner() {
return (list1, list2)-> { // 두번째 콘텐트와 합쳐 첫번째 누적자 고침
list1.addAll(list2);
return list1;
}
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(
IDENTITY_FINISH, CONCURRENT)); // 컬렉터 플러그 설정
}
}
기존의 코드를 다음과 같이 사용 가능하다.
List<Dish> dishes = menuStream.collect(new ToListCollector<Dish>());
6.6 커스텀 컬렉터를 구현해서 성능 개선하기
커스텀 컬렉터를 이용하여 소수와 비소수를 분류했던 예제의 성능을 개선해보자.
1 - 소수로만 나누기
이번에도 대상 숫자의 제곱근보다 작은 소수만 사용하도록 코드를 최적화
// 다음 소수가 대상 루트보다 크면 소수로 나누는 검사를 멈추도록 takeWhile 메서드 구현
// 대상 숫자 범위가 크다면 대상의 제곱보다 큰소수를 찾으면 검사를 중단
public ststic boolean isPrime(List<Integer> primes, int candidate) {
int candidateRoot = (int) Math.sqrt((double) candidate);
return primes.stream()
.takeWhile(i -> i <= candidateRoot)
.noneMatch(i -> candidate % i == 0);
}
1단계 : Collector 클래스 시그니처 정의
Collector 인터페이스 정의를 참고해 시그니처를 만들자.
public interface Collector<T, A, R>
public class PrimeNumbersCollector
implements Collector<integer, //스트림요소 형식
Map<Boolean, List<Integer>>, //누적자형식
Map<Boolesn, List<Integer>>> // 수집연산 결과형식
2단계 : 리듀싱 연산 구현
Collector 인터페이스에 선언된 다섯 메서드 구현
// supplier 메서드는 누적자를 만드는 함수를 반환
public Supplier<Map<Boolean, List<Integer>>> supplier() {
return ()-> new HashMap<Boolean, List<Integer>>() {
{
put(true, new ArrayList<Integer>());
put(false, new ArrayList<Integer>());
}
};
}
// 누적자를 만들면서 true, false 키와 빈리스트로 초기화
// 이제 수집과정에서 빈리스트에 각각 소수와 비소수를 추가할것이다.
스트림의 요소를 어떻게 수집할지 결정하는것은 accumulator 메서드. 컬렉터에서 가장 중요한 메서드
이제 원할때 수집과정 중간결과, 즉 지금까지 발견한 소수를 포함하는 누적자에 접근 가능
public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
return (Map<Boolean, List<Integer>> acc, Integer candidate)-> {
acc.get(isPrime(acc.get(true), candidate) ) //isPrime결과에 따라 소수,비소수 리스트 생성
.add(candidate); // candidate를 알맞은 리스트에 추가
}
}
3단계: 병렬 실행할 수 있는 컬렉터 만들기
병렬수집과정에서 두 부분 누적자를 합칠 수 있는 메서드를 만들자
public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
return(Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
map.get(true).addAll(map2.get(true));
map.get(false).addAll(map2.get(false));
return map1;
}
}
// 알고리즘 자체가 순차적이어서 컬렉터를 실제 병렬로 쓸수없어
// combiner메서드 호출될리 없어 빈구현으로 남겨두거나
// UnsupportedOperationException 던지도록 구현도 좋음
4단계: finisher 메서드와 컬렉터의 characteristics 메서드
//accumulator 형식을 컬렉터 결과 형식과 같으므로 변환과정 필요x
// 항등함수 identity를 반환하도록 구현
public Function<Map<Boolean, List<Integer>>,
Map<Boolean, List<Integer>>> finisher() {
return Function.identity();
}
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
}
이제 6.4 에서 팩토리메서드 partitioningBy 를 이용했던 예제를 커스텀 컬렉터로 교체할 수 있다.
public Map<Boolean, List<Integer>> partitionPrimesWithCustomCollector(int n) {
return IntStream.rangeClosed(2, n).boxed()
.collect(new PrimeNumbersCollector());
}
2 - 컬렉터 성능 비교
public class CollectorHarness {
public static void main(String[] args) {
long fastest = Long.MAX_VALUE;
for(int i=0; i<10; i++) { // 테스트 10번 반복
long start = System.nanoTime();
partitionPrimes(1_000_000); //백만개 숫자를 소수와 비소수로 분할
long duration = (System.nanoTime() - start)/1_000_000;
if(duration < fastest) fastest = duration; //가장빨리 실행됐는지 확인
}
System.out.println("Fastest execurion done in "+fastest+ "msecs");
}
}
partitionPrimes를 partitionPrimesWithCustiomCollector로 바꾼후 실행해보자.
커스텀클래스로 성능까지 향상 시켰다.
- collelct 는 스트림 요소를 요약 결과로 누적하는 다양한 방법을 인수로 갖는 최종연산
- 스트림의 요소를 ㅏ나의 값으로 리듀스하고 요약하는 컬렉터 뿐 아니라, 최소값,최대값,편균값을 계산하는 컬렉터등이 미리 정의
- 미리 정의된 컬렉터린 groupingBy로 스트림의 요소를 그룹화 하거나, partitioningBy로 스트림 요소 분할 가능
- 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계
- Collector인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발 가능.
'IT Book Summary > ModernJavaInAction' 카테고리의 다른 글
Part 3 스트림과 람다를 이용한 효과적 프로그래밍 - Chapter 8 컬렉션API 개선 (0) | 2020.03.29 |
---|---|
Chapter7 병렬 데이터 처리와 성능 (2) | 2020.03.22 |
Chapter5 스트림 활용 (0) | 2020.03.11 |
Part 2 함수형 데이터 처리 - Chapter 4 스트림 소개 (3) | 2020.03.02 |
Chapter3 람다 표현식_02 (1) | 2020.03.01 |