스트림의 연산은 아래 2개의 기준으로 구분이 가능
중간 연산 : filter, map..
최종 연산 : count, findFirst, forEach, reduce..
중간 연산
최종 연산
함수형 프로그래밍에서는 무엇
을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경 쓸 필요가 없음
Collector Interface를 구현해서 스트림의 요소를 어떤 식으로 도출할지 지정
다수준으로 그룹화를 수행할 때 명령형과 함수형의 차이점이 더욱 두드러짐
미리 정의된 컬렉터
groupingBy 같이 Collectors 클래스
에서 제공하는 팩토리 메서드
의 기능
Collectors에서 제공하는 메서드의 기능은 크게 세 가지로 구분
리듀싱과 요약
counting() = 팩토리 메서드가 반환하는 컬렉터
List.of("A", "B", "C", "D")
.stream()
.collect(Collectors.counting());
import static java.util.stream.Collectors.*;
좀더 간단하게 구현할 수 있다.
collectors.counting() -> counting()
최댓값과 최솟값
// Comparator 구현
Comparator<Transaction> comparator =
Comparator.comparingInt(Transaction::getYear);
// Collectors.maxBy()에 comparator pass
Optional<Transaction> mostTransaction =
transactionList.stream().max(comparator);
요약 연산
Collectors class는 요약 팩토리 메소드
를 제공합니다.
IntSummaryStatistics result =
transactionList.stream()
.collect(Collectors.summarizingInt(Transaction::getValue));
Double result =
transactionList.stream()
.collect(Collectors.averagingInt(Transaction::getValue));
IntSummaryStatistics의 toString(), 여러 요약 정보들이 포함
@Override
public String toString() {
return String.format(
"%s{count=%d, sum=%d, min=%d, average=%f, max=%d}",
this.getClass().getSimpleName(),
getCount(),
getSum(),
getMin(),
getAverage(),
getMax());
}
문자열 연결
컬렉터에 joining 팩토리 메소드를 이용하면 스트림의 각 객체에 toString메소드를 호출해 모든 문자열을 하나의 문자열로 연결해서 반환
// 요소의 name을 하나의 문자열로 만듬
String shortMenu = menu.stream().map(Dish::getName).collect(joining());
범용 리듀싱 요약 연산
reducing 팩토리 메소드로 이때까지의 많은 기능을 구현할 수 있습니다.
하지만 표현력과 가독성을 위해 특화된 기능을 사용하자
int totalCalories = menu.steam().collect(reducing(0, Dish::getCalories, (i, j) -> i + j))
// 인자가 1개인 reducing은 첫 번째 인자(리듀싱 연산의 시작값, 인수가 없을 때 반환값)에 스트림의 첫 번째 요소를 넣음
// 두 번째는 자신을 그대로 반환하는 항등 함수(identity function)를 넣음
// 고로 빈 스트림이 넘어온다면 시작값이 설정되지 않는 문제가 있어
// Optional<?>을 사용함
Optional<Dish> mostCalories =
menu.stream().collect(reducing(
(d1, d2) -> d1.getCalories() > d2.getCalories() ? d1: d2
));
아래와 같이 Collectors.toList()
도 reduce로 구현
하지만 의미론적으로 reduce는 두 값을 하나로 도출하는 불변형 연산이나
Collectors의 기능은 결과를 누적하는 컨테이너를 바꾸는 것
엄연히 다른 케이스입니다.
그리고 첫 번째 인자의 공유될 수 있는 객체가 존재하여 여러 스레드가 동시에
같은 데이터 구조체를 고치면 리스트 자체가 망가져버릴수 있습니다.
List<Integer> numbers = List.of(1, 2, 3, 4).stream()
.reduce(
new ArrayList<Integer>(),
(List<Integer> i, Integer e) -> {
i.add(e);
return i; },
(List<Integer> i2, List<Integer> e2) -> {
i2.addAll(e2);
return i2;}
);
reduce()를 사용하면
BiFunction<T,T,T>
타입의 인자 1개를 받아R apply(T t, U u)
를 수행합니다. R타입은 reduce()로 넘어온 중간연산의 결과 타입으로 매칭이 됩니다.
그룹화
collectors.groupingBy()를 통해 쉽게 그룹화할 수 있습니다.
Dish의 Type
을 기준으로 DishList를 그룹화하는 작업입니다.
이러한 것을 분류 함수(classification function)
이라고 합니다.
Map<Dish.Type, List<Dish`>> dishesByType =
menu.stream().collect(groupingBy(Dish::getType));
단순한 속성 접근자 대신 더 복잡한 분류 기준이 필요하다면 메서드 참조
로는 부족합니다.
예를 들면 400칼로리 이하는 'diet', 400~ 700은 'normal', 700초과는 'fat'으로 분류한다고 하면 람다식을 직접 작성해야합니다.
.. enum CaloricLevel { DIET, NORMAL, FAT }
Map<CaloricLevel, List<Dish>> dishedByCaloricLevel =
menu.stream().collect(
groupingBy(dish -> {
if(dish.getCalories() <= 400 ) return CaloricLevel.DIET;
else if(dish.getCalories() <= 700) return ..NORMAL;
else return CaloricLevel.FAT;
}));
그룹화된 요소 조작
요소를 그룹화 한 다음에 각 글룹의 요소를 조작하는 연산을 알아봅니다.
500칼로리 이상의 요리만 필터링해보기
// 그룹화 전 프레디케이트를 적용
Map<Dish.Type, List<Dish>> caloricDishedByType =
menu.stream().filter(dish -> dish.getCalories() > 500)
.collect(groupingBy(Dish::getType));
단점 : MEAT Type, FISH Type이 존재한는데 500칼로리 이상에 FISH Type이 없다면 결과에서 FISH Type key가 아예 소멸
filtering method에 또 다른 정적 팩토리 메소드로 프레디케이트를 넣어줍니다.
이 프레디케이트로 각 그룹의 요소와 필터링 된 요소를 재그룹화 합니다.
Map<Type, List<Menu>> caloricDishedType =
menus.stream()
.collect(Collectors.groupingBy(Menu::getType,
Collectors.filtering(menu -> menu.getCalories() > 500,
Collectors.toList())));
// 조건에 충족한게 없는 그룹도 출력(FISH)
{MEAT=[chzzBeef, FatFood], FISH=[]}
Collectors.mapping
함수를 이용해 요소를 변환하는 작업이 가능합니다.
map의 value가 mapping 값으로 변한다는 것을 유추할 수 있습니다.
추가 : Collectors.mapping(Menu::getName...
변경 : Map<Type, List<Menu'>>
-> Map<Type, List<String'>>
Map<Type, List<String>> caloricDishedType =
menus.stream()
.collect(Collectors.groupingBy(Menu::getType,
Collectors.mapping(Menu::getName,
Collectors.toList())));
Collectors.flatMapping
을 활용한 예시
menu에 존재하는 요리 타입의 태그들을 추출할 수 있습니다.
Map<Type, List<String>> menuTags = new HashMap<>();
menuTags.put(Type.MEAT, Arrays.asList("salty", "roasted"));
menuTags.put(Type.FISH, Arrays.asList("greasy", "salty"));
Map<Type, Set<String>> menuNamesByType =
menus.stream()
.collect(Collectors.groupingBy(Menu::getType,
Collectors.flatMapping(menu -> menuTags.get(menu.getType()).stream(),
Collectors.toSet()) // 반환 entry의 value type을 지정
));
Collectors.flatMapping(..,
Collectors.toSet()
)을 통해 반환 Value타입을 지정합니다. (중복 제거 효과)
결과적으로 Collectors.groupingBy
에서는 그루핑 기준과 조작 내용 2개를 받아 요소를 조작할 수 있습니다.
.collect(Collectors.groupingBy(
// 그루핑 기준
Menu::getType,
// 요소 조작
Collectors.flatMapping(menu -> menuTags.get(menu.getType()).stream(), Collectors.toSet())
)
);
다수준 그룹화
그룹 기준이 2개 이상을 의미하며 요소 조작과 같이 Collectors.groupingBy()
를 사용합니다.
menu.stream().collect(
groupigBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalories() <= 400)
return caloricLevel.DIET;
else if (dish.getCalories() <= 700)
returnn CaloricLeve.Normla;
else
retrun CaloricLevel.FAT
})
)
Dish의 Type을 기준으로 1차 그룹화를 합니다. -> (Dish::getType)
그 후 Dish의 Calories를 기준으로 다시 그룹화 합니다. -> (dish -> { ... return CaloricLevel... })
서브 그룹으로 데이터 수집
DishType마다 포함되는 개수 반환
= groupingBy(Disg::getType, Collector.counting())
groupingBy()를 넣은 것과 달리 다른 형식 반환 -> Map<DishType, Long>
DishType마다 제일 높은 칼로리 Dish 1개반환
= groupingBy(Disg::getType, maxBy(comparingInt(Dish::getCalories)))
Optional Type 반환 -> <DishType, Optional<Dish>>
maxBy가 생성하는 컬렉터의 결과 형식에 따라 맵의 값이 Optional로 변환
하지만 그룹화 맵에 새로운 키를 게으르게 추가하기 때문에 굳이 Optional wrapper를 사용 할 필요가 없다.
컬렉터 결과를 다른 형식에 적용하기
Map<DishType, Dish> mostCaloricByType = menus.stream()
.collect(Collectors.groupingBy(Menu::getType,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparingInt(Menu::getCalories)),
O
ptional::get)));
팩토리 메소드 collectingAndThen
은 1. 적용할 컬렉터와 2. 변환 함수를 인수로 받아 다른 컬렉터를 반환합니다.
=collectiong 결과를 받아 한번 더 가공하는 것`
groupingBy와 함께 사용하는 다른 컬렉터 예제
mapping(... return 원하는 반환 타입
) 같이 사용하여 groupingBy의 결과를 Menu에서 CaloricLevel로 변경합니다.
그리고 menu.collect(groupingBy(), toCollection()) 과 같이 toCollection()을 사용해 Map의 Value의 자료구조를 변경합니다.
Map<DishType, Set<CaloricLevel>> caloricLevelsByType =
menu.stream().collect(
groupingBy(Dish::getType, mapping(dish -> {
if(dish.getCalories() <= 400) return CaloricLevle.DIET;
else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
else return CaloricLevel.FAT;},
toCollection(HashSet::new) )));
)