이번 장에서는 다음과 같은 요소의 컬렉터를 배우게 된다.
long howManyDishes = menu.stream().collect(Collectros.counting());
long howManyDishes = menu.stream().conut();
Collectors.maxBy
, Collectors.minBy
두개의 메소드를 이용해서 스트림의 최댓값과 최솟값을 계산할 수 있다.Optional<Dish> mostCaloriesDish = menu.stream().collect(maxBy(dishCaloriesComparator));
Optional<Dish> leastCaloriesDish = menu.stream().collect(minBy(dishCaloriesCcomparator));
Collectors.summingInt
라는 특별한 요약 메소드를 제공.int totalCalroies = menu.stream().collect(summingInt(Dish::getCalories));
Collectors.summingLong
과Collectors.summingDouble
메소드 또한 같은 방식으로 동작
Collectors.averagingInt
, averagingLong
, averagingDouble
등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.double avgCalories = menu.stream().collect(avergingInt(Dish::getCalories));
summarizingInt
가 반환하는 컬렉터를 사용할 수 있다.IntSummaryStatistics menuStatistics = menu.stream().collect(summarizingInt(Dish::getCalories));
그러면 다음과 같은 정보를 수집할 수 있다.
IntSummaryStatistics{count=9, sum=4300, min=120, average= 477.7, max=800}
joining
팩토리 메소드를 이용하면 스트림의 각 객체에 toString 메소드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결하여 반환한다.String shortMenu = menu.stream().map(Dish::getName).collect(joining());
joining 메소드는 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만든다. Dish 클래스가 요리명을 반환하는 toString 메소드를 포함하고 있다면 다음 코드에서 보여주는 것처럼 각 요리의 이름을 추출하는 과정을 생략할 수 있다.
String shortMenu = menu.stream().collect(joining());
String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
reducing
팩토리 메소드로도 정의할 수 있다. 즉, 범용 Collectors.reducing으로도 구현할 수 있다.Collect와 Reduce
Collect와 Reduce , 둘 중 어느 것을 사용해도 원하는 결과를 사용할 수 있었다. 그러나 의미론적인 문제에서 둘의 차이가 발생한다.
Collect
는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메소드인 반면,Reduce
는 두 값을 하나로 도출하는 불변형 연산이라는 점에서 의미론적인 문제가 일어난다.즉, reduce 메소드가 누적자로 사용된 리스트를 변환시킨다면, 문법에는 맞을진 모르지만 의미론적으로는 틀린것이 된다.
reducing
컬렉터를 사용한 이전 예제에서 람다 표현식 대신 Integer 클래스의 sum 메소드 참조를 이용하면 코드를 좀 더 단순화할 수 있다.int totalCalories = menu.stream().collect(reducing(0, // 초기값
Dish::getCalories, //합계함수
Integer::sum)); //변환함수
// 메뉴의 타입에 따라 메뉴를 그룹화 하는 함수.
Map<Dish.Type, List<Dish>> dishesByType =
meun.stream().collect(groupingBy(Dish::getType));
groupingBy
메소드를 통해서 스트림이 그룹화되므로 이를 분류 함수 라고 부른다.diet
, 400~700칼로리를 normal
로, 700칼로리 초과를 fat
요리로 분류한다고 가정했을때, Dish 클래스에는 이러한 연산에 필요한 메소드가 없으므로 메소드 참조를 분류함수로 사용할 수 없다.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, List<Dish>> caloricDishesByType =
menu.stream().filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
위 코드로 문제를 해결할 수 있지만, 단점도 존재한다. 우리의 메뉴 요리는 다음처럼 맵 형태로 되어 있으므로 우리 코드에 위 기능을 사용하려면 맵에 코드를 적용하여야 한다.
그러나 만약 filtering된 요소에 하나의 Key값도 존재하지 않게 된다면, 그 Key값은 Collect된 맵 자체에서 사라지게 된다.
위와같은 문제를 해결하기 위해서는 다음과 같이 해결할 수 있다.
Map<dish.Type, List<Dish>> caloricDishesByType =
menu.stream()
.collect(groupingBy(Dish::getType,
filtering(dish -> dish.getCalories() > 500, toList())));
Collectors.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;
})));
Map<Dish.Type, Optional<<Dish>> mostCaloricByType =
menu.stream()
.collect(groupingBy(Dish::getType,maxBy(comparingInt(Dish::getCalories))));
Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType, //분류함수
collectingAndThen(
maxBy(comparingInt(Dish::getCalories)),// 감싸인 컬렉터
Optional::get //변환함수
)));
Map<Boolean, List<Dish>> partitionMenu = menu.stream()
.collect(partitioningBy(Dish::isVegetarian));
public interface Collector<T, A, R>{
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
Function<A, R> finisher();
BinaryOperator<A> combiner();
Set<Characteristics> characteristics();
}
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>
누적 과정에서 사용되는 갹체가 수집과정의 최종결과로 사용된다.
public Supplier<List<T>> supplier() {
return () -> new ArrayList<T>();
}
//혹은 생성자 참조를 전달하는 방법이다.
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
accmulator
메소드는 리듀싱 연산을 수행하는 함수를 반환한다. public BiConsumer<List<T>,T> accumulator() {
return (list, item) -> list.add(item);
}
// 메소드 참조를 이용하면 코드가 더 간결해진다.
public BiConsumer<List<T>,T> accumlator(){
return List::add;
}
public Function<List<T>, List<T>> finisher() {
return Function.identity();
}
combiner
combiner
는 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다.public BinaryOperator<List<T>> combiner(){
return (list1, list2) -> {
list1.addAll(list2);
return list1;
}
}
characterstics
메소드는 컬렉터의 연산을 정의하는 Characteristics
형식의 불변 집합을 반환한다.Characteristics
는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스 한다면 어떤 최적화를 선택해야 할지 힌트를 제공한다.accumulator
함수를 동시에 호출할 수 있으며, 이 컬렉터는 스트림의 병렬 리듀싱을 수행할 수 있다. 컬렉터의 플래그에 UNORDERED
를 함께 설정하지 않았다면 데이터 소스가 정렬되지 않은 상황에서만 병렬 리듀싱을 수행할 수 있다.identity
를 적용할 뿐 이므로 이를 생략할 수 있다. 따라서 누적자 A를 결과 R로 안전하게 형변환할 수 있다.