티스토리 뷰
728x90
반응형
- 스트림은 데이터 집합을 처리하는게 게으른 반복자라고 말하고 있습니다. 게으른 반복자라는 명칭이 붙은 이유는 최종 연산이 있기 전까지 수행하지 않기 때문에입니다.
- 중간 연산은 스트림 파이프라인을 구성하며, 스트림의 요소를 소비하지 않습니다. 반면 최종 연산은 스트림의 요소를 소비해서 최종 결과를 도출합니다.
- 스트림에서 다양한 누적 방식은 Collector 인터페이스에 정의되어 있습니다.
컬렉터란 무엇인가?
- Collector는 java.util.stream에 있는 인터페이스입니다.
- Collector 인터페이스 구현은 스트림의 요소를 어떤 방법으로 출력할지 지정합니다.
- 인수로 전달한 Collectors.toList()등은 Collector 인터페이스의 구현체일까요? 답은 아닙니다. 입니다.
- 가능한 이유는 Collectors 클래스의 내부 클래스인 CollectorImpl 클래스가 Collector 인터페이스를 구현하고 있고 toList와 같은 정적 메서드가 호출될 때마다 CollectorImpl을 만들어 반환하는 형식이기 때문입니다.
public final class Collectors {
static class CollectorImpl<T, A, R> implements Collector<T, A, R> {
}
}
리듀싱과 요약
counting : Collectors에서 제공하는 함수로 요소의 개수를 반환합니다.
long total = MENU.stream().collect(Collectors.counting());
long total = MENU.stream().count();
minBy, maxBy : 최솟값, 최댓값을 반환합니다.
Optional<Dish> max = MENU.stream().collect(maxBy((c1, c2) -> Integer.compare(c1.getCalorie(), c2.getCalorie())));
Optional<Dish> min = MENU.stream().collect(minBy((c1, c2) -> Integer.compare(c1.getCalorie(), c2.getCalorie())));
💡 요약 연산
- summingInt를 사용하면 초기값이 무조건 0이지만 reducing을 사용하여 초기값을 세팅하면 상황에 따라 더 유연하게 코드를 짤 수 있을거 같습니다.
- summarizingInt를 사용하면 동시에 합계, 평균등을 구할 수 있습니다.
int totalCalorie = MENU.stream().collect(summingInt(Dish::getCalorie)); // 누적 초기값은 0부터 시작
int reduceTotalCalorie = MENU.stream().collect(reducing(0, Dish::getCalorie, (i, j) -> i + j)); // 누적 초기값을 설정할 수 있음, 0대신 설정하고 싶은 값
int reduceTotalCalorie2 = MENU.stream().collect(reducing(0, Dish::getCalorie, Integer::sum)); // 메서드 참조를 사용하여 sum 구함
IntSummaryStatistics statistics = MENU.stream().collect(summarizingInt(Dish::getCalorie));
System.out.println(statistics); // IntSummaryStatistics{count=10, sum=4650, min=120, average=465.000000, max=800}
💡 문자열 연결
- joining 메서드를 이용하면 스트림의 각 객체에 toString 메서드를 호출해서 추출한 모든 문자열을 하나의 문자열로 연결해서 반환합니다.
- 내부적으로 StringBuilder를 이용해서 문자열을 하나로 만듭니다.
String names = MENU.stream().map(Dish::getName).collect(joining(", "));
💡 범용 리듀싱 요약 연산
- 지금까지 살펴본 모든 컬렉터는 reducing 팩토리 메서드로도 정의할 수 있습니다.
- 즉 범용 Collectors.reducing으로도 구현할 수 있습니다.
- 그럼에도 범용대신 위의 예제를 표현한 이유는 편의성 및 가독성 때문입니다.
- 첫번째 인자 - 리듀싱 연산의 시작값이거나 스트림에 인수가 없을 때 반환하는 값입니다.
- 두번째 인자 - 각 요소에 적용할 변환 함수입니다.
- 세번쩨 인자 - 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator입니다.
- reducing 메서드는 인자를 하나만 받을 수도 있는데 이때는 첫번째 인자가 스트림의 첫번째 요소가 되고, 두번째 인자는 자신을 반환하는 identity가 됩니다.
// Collectors 클래스의 reducing 메서드
public static <T, U> Collector<T, ?, U> reducing(U identity, Function<? super T, ? extends U> mapper,BinaryOperator<U> op) {
...
}
// 예제
MENU.stream().collect(reducing(
0 // 초기값,
Dish::getCalorie // 변환 함수,
Integer::sum // 누적 함수));
// 하나의 인자만 사용하는 reducing
Optional<Dish> 칼로리가_높은_음식 = MENU.stream()
.collect(reducing((dish1, dish2) -> dish1.getCalorie() > dish2.getCalorie() ? dish1 : dish2));
그룹화
- groupingBy 함수를 사용하면 쉽게 그룹핑을 할 수 있습니다.
- 스트림을 사용하지 않으면 코드가 엄청 지저분해지는것을 알 수 있습니다.
Map<Type, List<Dish>> 그룹으로_묶은_음식 = MENU.stream().collect(groupingBy(Dish::getType));
// 결과
{
FISH=[Dish(name=salmon, vegetarian=false, calorie=450, type=FISH), Dish(name=prawns, vegetarian=false, calorie=300, type=FISH), Dish(name=salmon, vegetarian=false, calorie=450, type=FISH)],
MEAT=[Dish(name=pork, vegetarian=false, calorie=800, type=MEAT), Dish(name=beef, vegetarian=false, calorie=700, type=MEAT), Dish(name=chicken, vegetarian=false, calorie=400, type=MEAT)],
OTHER=[Dish(name=french fries, vegetarian=true, calorie=530, type=OTHER), Dish(name=rice, vegetarian=true, calorie=350, type=OTHER), Dish(name=season fruit, vegetarian=true, calorie=120, type=OTHER), Dish(name=pizza, vegetarian=true, calorie=550, type=OTHER)]
}
// 스트림을 사용하지 않은 예
Map<Type, List<Dish>> 그룹으로_묶은_음식 = new HashMap<>();
List<Dish> 물고기리스트 = new ArrayList<>();
List<Dish> 육류리스트 = new ArrayList<>();
List<Dish> 다른리스트 = new ArrayList<>();
for (Dish dish : MENU) {
switch (dish.getType()) {
case FISH:
물고기리스트.add(dish);
break;
case MEAT:
육류리스트.add(dish);
break;
case OTHER:
다른리스트.add(dish);
break;
}
}
그룹으로_묶은_음식.put(Type.FISH, 물고기리스트);
그룹으로_묶은_음식.put(Type.MEAT, 육류리스트);
그룹으로_묶은_음식.put(Type.OTHER, 다른리스트);
// 칼로리로 음식 나누기
Map<String, List<Dish>> 칼로리_음식_묶음 = MENU.stream()
.collect(groupingBy(dish -> {
if (dish.getCalorie() <= 400) return "DIET";
else if (dish.getCalorie() <= 700) return "NOMAL";
else return "FAT";
}));
💡 그룹화된 요소 조작
- 그룹화 이후에 각 그룹에 대해 요소를 조작해야 하는 경우가 있습니다. 이를 위해 groupingBy 메서드에 추가적인 인자를 넘길 수 있습니다.
- 첫번째 인자를 통해 그룹핑이 진행되고, 두번째 인자에 의해 추가적인 그룹핑이 진행됩니다.
// 문제가 발생하는 상황
Map<Type, List<Dish>> 그룹으로_묶은_음식 = MENU.stream()
.filter(dish -> dish.getCalorie() > 500)
.collect(groupingBy(Dish::getType));
// FISH는 없음
{
OTHER=[Dish(name=french fries, vegetarian=true, calorie=530, type=OTHER), Dish(name=pizza, vegetarian=true, calorie=550, type=OTHER)],
MEAT=[Dish(name=pork, vegetarian=false, calorie=800, type=MEAT), Dish(name=beef, vegetarian=false, calorie=700, type=MEAT)]
}
// 해결방안
Map<Type, List<Dish>> 그룹으로_묶은_음식 = MENU.stream()
.collect(groupingBy(Dish::getType, filtering(dish -> dish.getCalorie() > 500, toList())));
{
MEAT=[Dish(name=pork, vegetarian=false, calorie=800, type=MEAT), Dish(name=beef, vegetarian=false, calorie=700, type=MEAT)],
FISH=[],
OTHER=[Dish(name=french fries, vegetarian=true, calorie=530, type=OTHER), Dish(name=pizza, vegetarian=true, calorie=550, type=OTHER)]
}
💡 다수준 그룹화
- 두 인수를 받는 groupingBy 메서드를 통해 항목을 다수준으로 그룹화할 수 있습니다.
Map<Type, Map<String, List<Dish>>> 그룹으로_묶은_음식 = MENU.stream()
.collect(groupingBy(Dish::getType,
groupingBy(dish -> {
if (dish.getCalorie() <= 400) return "DIET";
else if (dish.getCalorie() <= 700) return "NOMAL";
else return "FAT";
})));
분할
- 분할은 분할 함수라 불리는 Predicate를 분류 함수로 사용하는 특수한 그룹화 기능입니다. 분할 함수는 boolean을 반환하므로 맵의 키는 Boolean입니다. 결과적으로 참, 거짓 두개의 그룹으로 분류됩니다.
Map<Boolean, List<Dish>> partitionedMenu = MENU.stream()
.collect(partitioningBy(Dish::isVegetarian));
Collector 인터페이스
- Collector 인터페이스의 시그니처와 다섯개의 메서드 정의는 다음과 같습니다.
- T : 수집될 스트림 항목
- A : 누적자로 수집 과정에서 중간 결과를 누적하는 객체 타입
- R : 수집 연산의 결과 객체 타입(보통은 컬렉션)
public interface Collector<T, A, R> {
Supplier<A> supplier();
BiConsumer<A, T> accumulator();
BinaryOperator<A> combiner();
Function<A, R> finisher();
Set<Characteristics> characteristics();
}
728x90
반응형
'스터디 > 모던 자바 인 액션' 카테고리의 다른 글
모던 자바 인 액션 - 9장 리펙터링, 테스팅, 디버깅 (0) | 2022.09.11 |
---|---|
모던 자바 인 액션 - 8장 컬렉션 API 개선 (0) | 2022.09.10 |
모던 자바 인 액션 - 4장 스트림 소개 (0) | 2022.09.04 |
모던 자바 인 액션 - 3장 람다 표현식 (0) | 2022.09.03 |
모던 자바 인 액션 - 2장 동작 파라미터화 코드 전달하기 (0) | 2022.09.02 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot redisson 분산락 구현
- spring boot 엑셀 다운로드
- 람다 표현식
- service based architecture
- JDK Dynamic Proxy와 CGLIB의 차이
- 레이어드 아키텍처란
- 자바 백엔드 개발자 추천 도서
- redis sorted set으로 대기열 구현
- polling publisher spring boot
- java ThreadLocal
- microkernel architecture
- 공간 기반 아키텍처
- transactional outbox pattern spring boot
- 서비스 기반 아키텍처
- transactional outbox pattern
- @ControllerAdvice
- 트랜잭셔널 아웃박스 패턴 스프링부트
- pipeline architecture
- java userThread와 DaemonThread
- pipe and filter architecture
- redis 대기열 구현
- spring boot redis 대기열 구현
- space based architecture
- redis sorted set
- spring boot redisson destributed lock
- spring boot excel download paging
- spring boot excel download oom
- spring boot poi excel download
- spring boot redisson sorted set
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함