Book/모던 자바 인 액션

[모던 자바 인 액션] Chapter 06. 스트림으로 데이터 수집 (2)

ghan2 2024. 10. 25. 18:24

6.4 분할

분할 함수라 불리는 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능이다.
분할 함수는 불리언을 반환하므로 맵의 키 형식은 Boolean이며, 참 또는 거짓을 가지는 두 개의 그룹으로 분류된다.

Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(Dish::isVegetarian));
// {false = [pork, beef, chicken, prawns, salmon],
// true=[french fires, rice, season fruit, pizza]}

true 값의 키로 맵에서 모든 채식 요리를 얻을 수 있다.

List<Dish> vegetarianDishes = partitionedMenu.get(true); // 채식인 요리

 

6.4.1) 분할의 장점

  • 분할 함수가 반환하는 참, 거짓 두 가지 요소의 스트림 리스트를 모두 유지한다.
  • 또한 컬렉터를 두 번째 인수로 전달할 수 있는 오버로드된 버전의 partitioningBy 메서드도 있다.
    Collector partitioningBy(Predicate predicate) partitioningBy(Predicate<? super T>) 
    

Collector partitioningBy(Predicate predicate, Colelctor downstream) partitioningBy(Predicate<? super T>, Collector<? super T, A, D>)

채식 요리와 채식이 아닌 요리의 각각 그룹에서 가장 칼로리가 높은 요리를 찾을 수 있다.
```java
Map<Boolean, List<Dish>> partitionedMenu = menu.stream().collect(partitioningBy(
  Dish::isVegetarian, collectingAndThen(maxBy(comparingInt(Dish::getCalories)), Optional::get));
// {false=pork, true=pizza}

 

6.4.2) 숫자를 소수와 비소수로 분할하기
정수 n을 인수로 받아서 2에서 n까지의 자연수를 소수와 비소수로 나누는 프로그램을 구현하자.

// 주어진 수가 소수인지 아닌지 판별
public boolean isPrime(int candidate) {
    // 소수의 대상을 제곱근 이하의 수로 제한할 수도 있다.
    int candidateRoot = (int) Math.sqrt((double) candidate);

    return IntStream.range(2, candidateRoot).noneMatch(i -> candidate % i == 0);
}

이제 숫자 n을 받은 후에 프레디케이트와 partitioningBy 컬렉터로 소수와 비소수로 분할할 수 있다.

public Map<Boolean, List<Integer>> partitionPrimes(int n) {
    return IntStream.rangeClosed(2, n).boxed().collect(partitioningBy(candidate -> isPrime(candidate)));
}

 

6.5 Collector 인터페이스

Collector 인터페이스는 리듀싱 연선(즉, 컬렉터)을 어떻게 구현할지 제공하는 메서드 집합으로 구성된다.
Collctor 인터페이스를 살펴보기 전에 6장을 시작하면서 살펴본 Factory Method인 toList()를 자세히 확인하자.

// Collector 인터페이스의 시그니처와 메서드 정의
public interface Collector<T, A, R> {
  Supplier<A> supplier();
  BiConsumer<A, T> accumulator();
  Function<A, R> finisher();
  BinaryOperator<A> combiner();
  Set<Characteristics> characteristics();
}
  • T는 수집될 스트림 항목의 제네릭 형식이다.
  • A는 누적자, 즉 수집 과정에서 중간 결과를 누적하는 객체의 형식이다.
  • R은 수집 연산 결과 객체의 형식(항상 그런것은 아니지만 대게 컬렉션 형식)이다.
// 예를 들어 Stream<T>의 모든 요소를 List<T>로 수집하는 ToListCollector<T>라는 클래스를 구현할 수 있다.
public class ToListCollector<T> implements Collector<T, List<T>, List<T>>

 

6.5.1) Collector 인터페이스의 메서드 살펴보기

이제 Collector 인터페이스에 정의된 다섯 개의 메서드를 하나씩 살펴보자.

먼저 살펴볼 4개의 메서드는 collect 메서드에서 실행하는 함수를 반환하는 반면, 다섯 번째 메서드 characteristics는 collect 메서드가 어떤 최적화(예를 들면 병렬화 같은)를 이용해서 리듀싱 연산을 수행할 것인지 결정하도록 돕는 힌트 특성 집합을 제공한다.

  • supplier 메서드 : 새로운 결과 컨테이너 만들기
    • 빈 결과로 이루어진 Supplier를 반환해야 한다. 즉, 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수다.
    • ToListCollector처럼 누적자를 반환하는 컬렉터에서는 빈 누적자가 비어있는 스트림의 수집 과정의 결과가 될 수 있다.
  • accumulator 메서드 : 결과 컨테이너에 요소 추가하기
    • 리듀싱 연산을 수행하는 함수를 반환한다.
    • 스트림에서 n번째 요소를 탐색할 때 두 인수를 함수에 적용한다.
    • 함수의 반환값은 void. 즉, 요소를 탐색하면서 함수에 의해 누적자 내부 상태가 바뀌므로 누적자가 어떤 값일지 단정할 수 없다.
  • finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기
    • 누적 과정을 끝낼 때 호출할 함수를 반환한다. 
    • ToListCollector에서 볼 수 있는 것처럼 누적자가 이미 최종 결과인 경우에는 항등 함수를 반환한다. (return Function.identity();)

Collector 인터페이스에 대한 이해를 돕는 그림..

  • combiner 메서드 : 두 결과 컨테이너 병합
    • 마지막으로 리듀싱 연산에서 사용할 함수를 반환하는 메서드이다. 
    • 스트림의 서로 다른 서브파트를 병렬로 처리할 때 누적자가 이 결과를 어떻게 처리할지 정의한다. 
    • toList에서는 스트림의 두 번째 서브파트에서 수집한 항목 리스트를 첫 번째 서브파트 결과 리스트의 뒤에 추가하면 된다. 

아주 신기하다..

  •  Characteristics 메서드
    • 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환한다.
    • Characteristics는 스트림을 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택할지 힌트를 제공한다. (UNORDERED, CONCURRENT, IDENTITY_FINISH)
    • 예시로 들고있는 ToListCollector는 스트림의 요소를 누적하는 데 사용한 리스트가 최종 결과 형식이므로 추가 변환이 필요 없다. 

6.5.2) 응용하기

지금까지 배운 메서드를 이용해서 만든 ToListCollector 이다 !

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {
	@Override
    public Supplier<List<T>> supplier() {
    	return ArrayList::new;
    }
    
    @Override
    public BiConsumer<List<T>, T> accumulator() {
    	return List::add;
    }
    
    @Override
    public Function<List<T>, List<T>> finisher() {
    	return Function.identity();
    }
    
    @Override
    public BinaryOperator<List<T>> combiner() {
    	return (list1, list2) -> {
        	list1.addAll(list2);
            return list1;
        };
    }
    
    @Override
    public Set<Characteristics> characteristics() {
    	return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
    }
}

 

컬렉터 구현을 만들지 않고도 커스텀 수집 수행하기

IDENTITY_FINISH 수집 연산에서는 인터페이스를 새로 구현하지 않고도 같은 결과를 얻을 수 있는데 이런식으로 사용한다. 

List<Dish> dishes = menuStream.collect(
	ArrayList::new, // 발행
    List::add,      // 누적
    List::addAll);  // 합침

간결하고 축약되어 있지만 가독성은 떨어진다. 또한 characteristics를 전달할 수 없다. 즉, 이렇게 하려면 IDENTITY_FINISH와 CONCURRENT지만 UNORDERED는 아닌 컬렉터로만 전달된다. 

 

6.7 마치며

  • collect는 스트림의 요소를 요약 결과로 누적하는 다양한 방범(컬렉터라 불리는)을 인수로 갖는 최종 연산이다. 
  • 스트림의 요소를 하나의 값으로 리듀스하고 요약하는 컬렉터뿐 아니라 최솟값, 최댓값, 평균값을 계산하는 컬렉터 등이 미리 정의되어 있다. 
  • 미리 정의된 컬렉터인 groupingBy로 스트림의 요소를 그룹화하거나, partitioningBy로 스트림의 요소를 분할할 수 있다. 
  • 컬렉터는 다수준의 그룹화, 분할, 리듀싱 연산에 적합하게 설계되어 있다.
  • Collector 인터페이스에 정의된 메서드를 구현해서 커스텀 컬렉터를 개발할 수 있다.