티스토리 뷰

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
반응형