모던 자바 인 액션: 스트림으로 데이터 수집

minseok·2023년 5월 22일
0

스트림의 연산은 아래 2개의 기준으로 구분이 가능
중간 연산 : filter, map..
최종 연산 : count, findFirst, forEach, reduce..

중간 연산

  • 한 스트림을 다른 스트림으로 변환하는 연산 -> 여러 연산을 연결
  • 스트림 파이프라인을 구성
  • 스트림의 요소를 소비(consume)하지 않음

최종 연산

  • 스트림의 요소를 소비해서 최종 결과를 도출
  • 스트림 파이프라인을 최적화하면서 계산 과정을 짧게 생략하기도 함


Collector

함수형 프로그래밍에서는 무엇을 원하는지 직접 명시할 수 있어서 어떤 방법으로 이를 얻을지는 신경 쓸 필요가 없음

Collector Interface를 구현해서 스트림의 요소를 어떤 식으로 도출할지 지정
다수준으로 그룹화를 수행할 때 명령형과 함수형의 차이점이 더욱 두드러짐

  • "각 요소를 리스트로 만들어라" = collect(Collectors.toList())
  • "각 키(통화) bucket, 각 키 버킷에 대응하는 요소 리스트를 값으로 포함하는 map을 만들어라" = collect(groupingBy(Transaction::getCurrency))

미리 정의된 컬렉터
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)));

팩토리 메소드 collectingAndThen1. 적용할 컬렉터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) )));
    )
profile
즐겁게 개발하기

0개의 댓글