본문 바로가기

프로그래밍/Book

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

컬렉터란 무엇인가?

컬렉터 인터페이스 구현은 스트림의 요소를 어떤 식으로 도출할지 지정한다.

리듀싱과 요약

컬렉터로 스트림의 모든 요소를 하나의 결과로 합칠 수 있다.

스트림값에서 최대값 최소값 검색

Collectors.maxBy()

  • 스트림의 최대값 계산
  • 스트림 요소를 비교하는데 Comparator를 인수로 받음

Collectors.minBy()

  • 스트림의 최소값 계산
  • 스트림 요소를 비교하는데 Comparator를 인수로 받음

요약 연산

스트림에 있는 객체의 숫자 필드의 합계나 평균 등을 반환하는 연산

Collectors.summingInt()

  • 객체를 int로 매핑하는 함수를 인수로 받음
  • int로 매핑된 요소의 총합 계산
  • summingLong(), summingDouble도 존재

Collectors.averagingInt()

  • 객체를 int로 매핑하는 함수를 인수로 받음
  • int로 매핑된 요소의 평균 계산
  • averagingLong(), averagingDouble도 존재

Collectors.summarizingInt()

  • 하나의 요약 연산으로 수, 합계, 평균, 최댓값, 최소값을 구할 수 있음
  • IntSummaryStatistics 클래스로 return
  • summarizingLong, summarizingDouble도 존재

문자열 연결

joining()

  • 스트림의 각 객체에 toString 메서드를 호출하여 추출한 모든 문자열을 하나의 문자열로 반환

범용 리듀싱 요약 연산

이전까지의 컬렉터는 reducing 메서드를 통해서도 정의할 수 있다.
reducing()

  • 첫 번째 인수는 리듀싱 연산의 시작 값이거나 스트림에 인수가 없는 경우 반환값
  • 두 번째 인수는 변환 함수
  • 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator
  • 한 개의 인수만 넘겨준다면 BinaryOperator를 넘겨야함

그룹화

데이터 집합을 하나 이상의 특성으로 분류해서 그룹화하는 작업
Collectors.groupingBy()

  • 스트림의 요소들을 인수로 넘기는 함수를 기준으로 그룹화하며 이 함수를 분류 함수라고 함
  • 그룹화 연산의 결과로 그룹화 함수가 반환하는 키와 각 키에 대응하는 스트림의 모든 항목 리스트를 값으로 갖는 Map반환
List<Book> books = asList(new Book("2022신상책", "홍길동", 2022, "IT"),
        new Book("이순신의 바다", "황현필", 2021, "History"),
        new Book("갈매기의 꿈", "리처드 바크", 2020, "Fiction"),
        new Book("모던 자바 인 액션", "라울-게이브리얼 우르마", 2020, "IT"),
        new Book("2019년 책", "홍길동", 2019, "IT"),
        new Book("이펙티브 자바", "조슈아 블로크", 2018, "IT")
);

Map<String, List<Book>> collect = books.stream().collect(Collectors.groupingBy(Book::getField));

결과

{IT=[Book{name='2022신상책', author='홍길동', year=2022, field='IT'}, Book{name='모던 자바 인 액션', author='라울-게이브리얼 우르마', year=2020, field='IT'}, Book{name='2019년 책', author='홍길동', year=2019, field='IT'}, Book{name='이펙티브 자바', author='조슈아 블로크', year=2018, field='IT'}], History=[Book{name='이순신의 바다', author='황현필', year=2021, field='History'}], Fiction=[Book{name='갈매기의 꿈', author='리처드 바크', year=2020, field='Fiction'}]}

그룹화된 요소 조작

요소를 그룹화한 후에 각 결과 그룹의 요소를 조작하는 연산이 필요함.
그룹화 전에 predicate로 필터를 적용해서 해결할 수도 있지만 이 경우 만족하는 결과가 없으면 키 자체가 사라진다.
groupingBy의 두 번째 인수로 필터 predicate를 줘서 이를 해결할 수 있다.

Map<String, List<Book>> collect2 = books.stream()
                .collect(Collectors.groupingBy(Book::getField, Collectors.filtering(book -> book.getYear() >= 2021, Collectors.toList())));
{IT=[Book{name='2022신상책', author='홍길동', year=2022, field='IT'}], History=[Book{name='이순신의 바다', author='황현필', year=2021, field='History'}], Fiction=[]}

mapping()

  • 매핑함수와 각 항목의 적용한 함수를 모으는데 사용하는 메서드
Map<String, List<String>> collect3 = books.stream()
                .collect(Collectors.groupingBy(Book::getField, Collectors.mapping(Book::getName, Collectors.toList())));

다수준 그룹화

두 인수를 받는 groupingBy를 사용하여 항목을 다수준으로 그룹화 할 수 있다.

 Map<String, Map<String, List<Book>>> collect4 = books.stream()
                .collect(Collectors.groupingBy(Book::getField, Collectors.groupingBy(book -> {
                    if (book.getYear() < 2020) {
                        return "old book";
                    } else {
                        return "new book";
                    }
                })));
{
        IT={
                new book=[
                        Book{name='2022신상책', author='홍길동', year=2022, field='IT'}, 
                        Book{name='모던 자바 인 액션', author='라울-게이브리얼 우르마', year=2020, field='IT'}], 
                old book=[
                        Book{name='2019년 책', author='홍길동', year=2019, field='IT'}, 
                        Book{name='이펙티브 자바', author='조슈아 블로크', year=2018, field='IT'}
                ]
        }, 
        History={
                new book=[
                        Book{name='이순신의 바다', author='황현필', year=2021, field='History'}
                ]
        },
        Fiction={
                new book=[
                        Book{name='갈매기의 꿈', author='리처드 바크', year=2020, field='Fiction'}
                ]
        }
}

분할

분할 함수라 불리는 predicate를 분류함수로 사용하는 특수한 그룹화 기능
Boolean을 반환하여 그룹화는 True인 그룹과 false인 그룹으로 분류된다.

partitioningBy()

  • 인수로 분할함수를 넘겨주면 true, false 두그룹으로 분류한 map을 반환
Map<Boolean, List<Book>> collect5 = books.stream()
        .collect(Collectors.partitioningBy(book -> book.getYear() < 2020));

Collector 인터페이스

리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합

public interface Collector<T, A, R>{
        Supplier<A> supplier();
        BiConsumer<A,T> accumulator();
        BinaryOperator<A> combiner();
        Function<A,R> finisher();
        Set<Characteristics> characteristics();
}
  • T: 수집될 스트림 항목의 제너릭 형식
  • A: 누적자. 수집 과정에서 중간 결과를 누적하는 객체의 형식
  • R: 수집 연산 결과 객체의 형식

supplier() - 새로운 결과 컨테이너 만들기

  • 빈 결과로 이루어진 supplier를 반환해야 함
  • 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
public Supplier<List<T>> supplier() {
    return ArrayList::new;
}

accumulator() - 결과 컨테이너에 요소 추가하기

  • 리듀싱 연산을 수행하는 함수 반환
  • n번째 요소 탐색시 누적자(n-1번쨰 요소까지 연산한 상태)와 n번쨰 요소를 함수에 적용
  • 반환값은 void
public BiConsumer<List<T>, T> accumulator() {
    return List::add;
}

finisher() - 최종 변환값을 결과 컨테이너로 적용하기

  • 스트림 탐색을 끝내고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 함
  • 반환값은 void
  • 누적자 객체가 이미 최종 결과인 경우 항등함수를 반환
public Function<List<T>, List<T>> finisher() {
    return Function.identity();
}

combiner() - 두 결과 컨테이너 병합

  • 리듀싱 연산에서 사용할 함수 반환
  • 서로 다른 서브 파트를 병렬로 처리할 때 누적자가 결과를 어떻게 처리할지 정의
  • 스트림의 두 번째 서브파트에서 수합한 리스트를 첫번째 서브파트 결과 리스트에 추가하면 됨
public BinaryOperator<List<T>> combiner() {
    return (list1, list2) -> {
        list1.addAll(list2);
        return list1;
    }
}

Characteristics()

  • 컬렉터의 연산을 정의하는 Characteristics형식으로 불변 집합을 반환
  • 스트림을 병렬로 리듀스할 것인지, 만약 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공
  • 아래 세 항목을 포함하는 열거형
    • UNOORDERED
      • 리듀싱 결과는 스트림의 요소의 방문 순서나 누적 순서에 영향을 받지 않음.
    • CONCURRENT
      • 다중 스레드에서 accumulator 함수를 동시에 호출할 수 있음.
      • 컬렉터의 플래그에 UNORDERED를 설정하지 않았다면 데이터 소스가 정렬되지 않은 상황에서만 병렬 리듀싱을 수행할 수 있음.
    • IDENTITY_FINISH
      • finisher 메서드가 반환하는 함수는 단순히 identity를 적용할 뿐이므로 생략 가능.
      • 따라서 리듀싱 과정의 최종 결과로 누적자 객체를 바로 사용할 수 있음.
public Set<Characteristics> characteristics() {
    return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
}

참고문서

모던 자바 인 액션 - 라울 게이브리얼 우르마, 마리오 푸스코, 앨런 마이크로소포트