Map<Currency, Integer>
을 반환해야 한다. Map<Currency, List<Transaction>> transactionByCurrencies = new HashMap<>(); // 그룹화한 트랜잭션을 저장할 맵을 생성
for (Transactin transaction : transactions) { // 트랜잭션 리스트를 반복
Currency currency = transaction.getCurrency();
List<Transaction> transactionsForCurrency = transactionsByCurrencies.get(currency);
if (transactionForCurrency = null) { // 현재 통화를 그룹화하는 맵에 항목이 없으면 항목을 만든다.
transactionsForCurrency = new ArrayList<>();
transactionsByCurrencies.put(currency, transactionsForCurrency);
}
transactinsForCurrency.add(transaction); // 같은 통화를 가진 트랜잭션 리스트에 현재 탐색 중인 트랜잭션을 추가
}
Map<Currency, List<Transaction>> transactionsByCurrencies = transactions.stream().collect(groupingBy(Transaction::getCurrency));
reduce 연산(하나로 수렴)을 통해 스트림을 다양한 형태로 collect 하는 방법에 대한 내용
고급 리듀싱
기능을 수행리듀싱 연산
을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리
한다.Collectors 유틸티리 클래스에는 자주 사용하는 컬렉터 인스턴스를 쉽게 생설할 수 있는 정적 팩터리 메서드를 제공한다.
Collectors에서 제공하는 메서드의 기능은 크게 세가지
다양한 계산을 수행할 때 사용하는 유용한 컬렉터
menu.stream().collect(Collectors.counting())
menu.stream().count()
Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(Comparator.comparingInt(Dish::getCalories)));
Collectors.summingInt
public static <T> Collector<T, ?, Integer> summingInt(ToIntFunction<? super T> mapper) {
return new CollectorImpl<>(
() -> new int[1],
(a, t) -> { a[0] += mapper.applyAsInt(t); },
(a, b) -> { a[0] += b[0]; return a; },
a -> a[0], CH_NOID);
}
@FunctionalInterface
public interface ToIntFunction<T> {
/**
* Applies this function to the given argument.
*
* @param value the function argument
* @return the function result
*/
int applyAsInt(T value);
}
int totalColories = menu.stream().collect(summingInt(Dish:getCalories));
객체를 int로 매핑하는 함수
를 인수로 받는다.summarizingInt()
IntSummaryStatistics statistic = menu.stream().collect(Collectors.summarizingInt(Dish::getCalories));
joining()
menu.stream().map(Dish::getName).collect(Collectors.joining(", "));
이러한 모든 Collector는 reducing 팩토리 메서드로도 정의할 수 있다. (가독성이나 편리성 측면에서 권장하지 않는다)
reducing()
public static <T, U> Collector<T, ?, U> reducing(
U identity,
Function<? super T, ? extends U> mapper,
BinaryOperator<U> op) {
...
}
초기값
. 스트림에 인수가 없을 때는 반환값.변환함수
.합계 함수
(BinaryOperator.) menu.stream().collect(Collectors.reducing(0, Dish::getCalories, (i, j) -> i + j)
menu.stream().collect(Collectors.reducing(0, Dish::getCalories, Integer::sum)
한개짜리 인수를 갖는 reducing()
public static <T> Collector<T, ?, Optional<T>> reducing(BinaryOperator<T> op) {
...
}
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
menu.stream().collect(Collectors.reducing((d1, d2) -> d1.getName() + d2.getName()))
맨 처음 트랜잭션 currency 그룹화 예제를 생각해보면 명령형으로 그룹화를 구현하려면 까다롭고, 에러도 신경써야 하고, 코드가 길다.
Collectors.groupingBy
이용하면 쉽게 그룹화가 가능하다.분류 함수
라고 부른다.Map<Dish.Type, List<Dish>> dishesByType = menu.stream().collect(Collectors.groupingBy(Dish::getType));
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.NORMAL;
} else {
return CaloricLevel.FAT;
}
}));
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;
}
})
)
);
// 결과
{FISH={NORMAL=[salmon], DIET=[prawns]}, OTHER={NORMAL=[fries, pizza], DIET=[rice, fruit]}, MEAT={FAT=[pork], NORMAL=[beef], DIET=[chicken]}}
Map<Dish.Type, Optional<Dish>> mostCaloricByType =
menu.stream().collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
// 결과
{OTHER=Optional[pizza], MEAT=Optional[pork], FISH=Optional[salmon]}
Map<Dish.Type, Dish> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType, // 분류함수
collectingAndThen(maxBy(comparingInt(Dish::getCalories)), // 감싸인 컬렉터
Optional::get))); // 변환함수. Optional에 포함된 값을 추출
분할은 분할 함수(프레디케이트)를 분류 함수로 사용하는 특수한 그룹화다
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
// 결과 : {false=[pork, beef, chicken, prawns, salmon], true=[french fries, rice, season fruit, pizza]}
Map<Boolean, Map<Dish.Type, List<Dish>>> vegetarianDishesByType = menu.stream().collect(
partitioningBy(Dish::isVegetarian, // 분할 함수
groupingBy(Dish::getType))); // 두 번째 컬렉터
// 결과 :
{false={MEAT=[pork, beef, chicken], FISH=[prawns, salmon]},
true={OTHER=[french fries, rice, season fruit, pizza]}}
지금까지 살펴본 모든 Collector는 Collector 인터페이스를 구현한다. Collector 인터페이스를 자세히 살펴보자.
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
public static <T> Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
새로운 결과 컨테이너 만들기
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
결과 컨테이너에 요소 추가하기
@FunctionalInterface
public interface BiConsumer<T, U> {
void accept(T t, U u);
public BiConsumer<List<T>, T> accumulator() {
return (list, item) -> list.add(item);
}
public BiConsumer<List<T>, T> accumulator() {
return List::add;
}
최종 변환 값을 결과 컨테이너로 적용하기
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
static <T> Function<T, T> identity() {
return t -> t;
}
}
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
위 3가지 메소드는 순차적 스트림 리듀싱 기능을 수행할 수 있다. 하지만 실제로 collect가 동작하기 전, 다른 중간 연산과 파이프라인을 구성할 수 있게 해주는 게으른 특성 그리고 병렬 실행 등도 고려해야 하므로 스트림 리듀싱 기능 구현은 생각보다 복잡하다.
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
}
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
public BinaryOperator<List<T>> combiner() {
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
UNORDERED
CONCURRENT
DENTITY_FINISH
finisher 메서드가 반환하는 함수는 identity를 적용할 뿐이므로 이를 생략할 수 있다.
따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있다.
또한 누적자 A를 결과 R로 안전하게 형변환 할 수 있다.
toList()
static final Set<Collector.Characteristics> CH_ID
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));