filter
, map
count
, findFirst
, forEach
, reduce
❗️기억하기
stream.collect() 는 인자로 Collector 인터페이스 인스턴스를 받고,
Collectors의 toList(), counting() 등은 Collector 형을 return한다.
collect
를 호출하면 스트림의 요소에 (컬렉터로 파라미터화된) 리듀싱 연산이 수행된다.collect
에서는 리듀싱 연산을 이용해서 스트림의 각 요소를 방문하면서 컬렉터가 작업을 처리한다.List<Transaction> transactions = transactionStream.collect(Collectors.toList());
toList()
Stream.collect
메서드의 인수)로 스트림의 항목을 컬렉션으로 재구성할 수 있다.long howManyDishes = menu.stream().collect(Collectors.counting()); // 방법 1
long howManyDishes2 = menu.stream().count(); // 방법 2
counting
컬렉터는 다른 컬렉터와 함께 사용할 때 위력을 발휘한다.Comparator
를 인수로 받는다.Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCaloriesDish = menu.stream()
.collect(maxBy(dishCaloriesComparator));
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
summingInt
는 객체를 int
로 매핑하는 함수를 인수로 받는다.public interface ToIntFunction<T> {
int applyAsInt(T value);
}
Collectors.summingLong
과Collectors.summingDouble
메서드는 같은 방식으로 동작하며 각각 long 또는 double 형식의 데이터로 요약한다는 점만 다르다.
Collectors.averagingLong()
, Collectors.averagingDouble
등으로 다양한 형식으로 이루어진 숫자 집합의 평균을 계산할 수 있다.joining()
팩토리 메서드를 이용하면 스트림의 각 객체에 toString
메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다.String shortMenu = menu.stream().map(Dish::getName).collect(joining(", "));
StringBuilder
를 이용해서 문자열을 하나로 만든다.joining
팩터리 메서드도 있다.지금까지 살펴본 모든 컬렉터는 다음에 나오는
reducing
팩토리 메서드로도 정의할 수 있다. 즉,Collectors.reducing
으로도 구현할 수 있다.
아래와 같이 reducing 메서드(인자 3개)로 만들어진 컬렉터로도 메뉴의 모든 칼로리 합계를 계산할 수 있다.
<인자 3개인 reducing>
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCalories, (i, j) -> i + j));
int totalCalories = menu.stream()
.collect(reducing(0, Dish::getCalories, (Integer::sum));
0
Dish::getCalories
Integer
로 변환할 때 사용한 변환 함수(i, j) -> i + j
BinaryOperator
<인자 1개인 reducing>
Optional<Dish> mostCaloriesDish = menu.stream()
.collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2));
➜ 값이 없는 빈 스트림이 넘겨졌을 때 시작값이 없어 문제가 생기므로 Optional<Dish>
객체를 반환하는 것
💡 collect와 reduce
- collect와 reduce로 같은 기능을 구현할 수 있다.
- 그러나 의미론적인 문제와 실용성 문제 등 두 가지 문제가 발생할 수 있다.
collect
: 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드reduce
: 두 값을 하나로 도출하는 불변형 연산
➜ 실용성 문제 발생 : 여러 스레드가 동시에 같은 데이터 구조체를 고치면 리스트 자체가 망가져버리므로 리듀싱 연산을 병렬로 수행할 수 없다는 점
groupby
)에서 많이 수행되는 작업이다.Map<Dish.Type, List<Dish>> dishesByType = menu.stream()
.collect(groupingBy(Dish::getType));
- 단순한 속성 접근자(
Dish::getType
등) 대신 더 복잡한 분류 기준이 필요한 상황에서는 메서드 참조를 분류 함수로 사용할 수 없다.- 메서드 참조 대신 람다 표현식으로 필요한 로직 구현 가능
요소를 그룹화 한 다음에는 요소들을 조작하는 연산이 필요하다.
Map<Dish.Type, List<Dish>> caloricDishesByType = menu.stream()
.collect(groupingBy(Dish::getType, Collectors.filtering(dish -> dish.getCalories() > 500, toList())));
결과
{OTHER=[french fries, pizza], MEAT=[pork, beef], FISH=[]}
Map<Dish.Type, List<String>> dishNamesByType = menu.stream()
.collect(groupingBy(Dish::getType, Collectors.mapping(Dish::getName, toList())));
결과로 Dish 객체가 아닌 요리 이름이 들어감
Map<Dish.Type, List<Dish>>
➜ Map<Dish.Type, List<String>>
칼로리 같은 한 가지 기준으로 메뉴의 요리를 그룹화했는데 두 가지 이상의 기준을 동시에 적용할 수 있을까?
➜ 효과적으로 조합할 수 있다는 것이 그룹화의 장점
Collectors.groupbingBy
는 일반적인 분류 함수와 컬렉터를 인수로 받는다.groupingBy
메서드에 스트림의 항목을 분류할 두 번째 기준을 정의하는 내부 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;
})
)
);
groupingBy
연산을 '버킷' 개념으로 생각하면 쉽다.groupingBy
는 각 키의 버킷을 만든다.groupingBy
컬렉터에 두 번째 인수로 counting
컬렉터를 전달할 수 있다.Map<Dish.Type, Long> typesCount = menu.stream().collect(
groupingBy(Dish::getType, counting()));
{MEAT=3, FISH=2, OTHER=4}
Map<Dish.Type, Dish> mostCaloricByType = menu.stream()
.collect(groupingBy(Dish::getType, // 분류 함수
collectingAndThen(maxBy(comparingInt(Dish::getCalories)), // 감싸진 컬렉터
Optional::get))); // 변환 함수
collectingAndThen
은 적용할 컬렉터와 변환 함수를 인수로 받아 다른 컬렉터를 반환한다.maxBy
로 만들어진 컬렉터가 감싸지는 컬렉터이며 변환 함수 Optional::get
으로 반환된 Optional
에 포함된 값을 추출한다.Optional.empty()
를 반환하지 않으므로 안전한 코드다.