Book

[모던 자바 인 액션] 6장. 스트림으로 데이터 수집

Jordy-torvalds 2021. 9. 28. 19:24
반응형

이전 장에서 공부했듯이 스트림은 엘리먼트를 소비하는 중간 연산과 최종 결과를 도출하는 최종연산으로 구분됩니다. 그 중에서 collect 최종 연산에 사용할 수 있는 리스트 변환 세트 변환 등의 기능이 담긴 인터페이스는 Collector 인터페이스 입니다. 여러 자료 구조가 담고 있는 Collection 인터페이스와 구분이 필요합니다.

컬렉터란?

간편하게 원하는 형태로 최종 결과를 산출해주는데 도움을 주는 인터페이스.

샘플 데이터

public class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }

    public enum Type { MEAT, FISH, OTHER }

    public static List<Dish> defaultList() {
        return Arrays.asList(
                new Dish("pork", false, 800, Type.MEAT),
                new Dish("beef", false, 700, Type.MEAT),
                new Dish("chicken", false, 400, Type.MEAT),
                new Dish("fries", true, 500, Type.OTHER),
                new Dish("rice", true, 350, Type.OTHER),
                new Dish("fruit", true, 120, Type.OTHER),
                new Dish("pizza", false, 600, Type.OTHER),
                new Dish("prawns", false, 300, Type.FISH),
                new Dish("salmon", false, 450, Type.FISH)
        );
    }
}

최대값과 최소값 검색

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = Dish.defaultList().stream()
        .collect(maxBy(dishCaloriesComparator));

요약 연산

// 합
int totalCalories 
= Dish.defaultList().stream()
.collect(summingInt(Dish::getCalories));

// 평균
double averageCalories 
= Dish.defaultList().stream()
.collect(averagingInt(Dish::getCalories));

// 요소수, 합, 최소, 평균, 최대
IntSummaryStatistics menuStatistics 
= Dish.defaultList().stream()
.collect(summarzingInt(Dish::getCalories));

문자열 연결

String shortMenu = Dish.defaultList().stream()
        .map(Dish::getName)
        .collect(Collectors.joining());

String shortMenuWithComma = Dish.defaultList().stream()
        .map(Dish::getName)
        .collect(Collectors.joining(", "));

범용 리듀싱 요약 연산

// reducing
// 1st param: initial value
// 2nd param: type casting function
// 3rd param: binary opeartor(in 1st param is initial element and identity function)

int totalCalories = Dish.defaultList().stream()
                .collect(reducing(0, Dish::getCalories, (i, j) -> i + j));

---

// 위와 동일한 연산 처리
int totalCalories = Dish.defaultList().stream()
                .map(Dish::getCalories)
                .reduce(0, (i, j) -> i + j);

그룹화

특정 기준을 기준으로 분류

Map<Dish.Type, List<Dish>> typeGrouping = Dish.defaultList().stream()
                .collect(groupingBy(Dish::getType));

조건절을 통한 분류

Map<String, List<Dish>> calorieGrouping = Dish.defaultList().stream()
                .collect(groupingBy(dish -> {
                    if (dish.getCalories() <= 400) {
                        return "DIET";
                    } else if (dish.getCalories() <= 700) {
                        return "NORMAL";
                    } else {
                        return "FAT";
                    }
                }));

다수준 그룹화

Map<Dish.Type, Map<Object, List<Dish>>> multiLevelGrouping = Dish.defaultList().stream()
                .collect(
                        groupingBy(Dish::getType,
                                groupingBy(dish -> {
                                    if (dish.getCalories() <= 400) {
                                        return "DIET";
                                    } else if (dish.getCalories() <= 700) {
                                        return "NORMAL";
                                    } else {
                                        return "FAT";
                                    }
                                }))
                        );

분할

Map<Boolean, List<Dish>> partitioningMenu = Dish.defaultList().stream()
                .collect(partitioningBy(Dish::isVegetarian));

Collection 인터페이스

리듀싱 연산을 어떻게 구현할지 제공하는 메소드 집합으로 구성되어 있습니다. 이 인터페이스를 이해하면 직접 리듀싱 연산을 만들 수도 있습니다.

/*
* T: 수집될 스트림 항목의 제네릭 형식
* A: 누적자. 수집 과정에서 중간 결과를 누적하는 객체의 형식
* R: 수집 연산 결과 객체의 형식
*/
public interface Collector<T, A, R> {
    /**
     * A function that creates and returns a new mutable result container.
     *
     * @return a function which returns a new, mutable result container
     */
    Supplier<A> supplier();

    /**
     * A function that folds a value into a mutable result container.
     *
     * @return a function which folds a value into a mutable result container
     */
    BiConsumer<A, T> accumulator();

    /**
     * A function that accepts two partial results and merges them.  The
     * combiner function may fold state from one argument into the other and
     * return that, or may return a new result container.
     *
     * @return a function which combines two partial results into a combined
     * result
     */
    BinaryOperator<A> combiner();

    /**
     * Perform the final transformation from the intermediate accumulation type
     * {@code A} to the final result type {@code R}.
     *
     * <p>If the characteristic {@code IDENTITY_FINISH} is
     * set, this function may be presumed to be an identity transform with an
     * unchecked cast from {@code A} to {@code R}.
     *
     * @return a function which transforms the intermediate result to the final
     * result
     */
    Function<A, R> finisher();

    /**
     * Returns a {@code Set} of {@code Collector.Characteristics} indicating
     * the characteristics of this Collector.  This set should be immutable.
     *
     * @return an immutable set of collector characteristics
     */
    Set<Characteristics> characteristics();
}
반응형