본문 바로가기
독서/모던 자바 인 액션

[자바] 스트림을 이용한 데이터 수집 - Collectors

by kriorsen 2023. 10. 5.

Collectors란?

Collectors는 스트림을 처리하고 요소를 수집하는 데 사용되는 유틸리티 클래스로, 해당 클래스의 메서드들을 사용하면 스트림을 쉽게 처리할 수 있습니다.

8.1 Collectors.counting()과 count()

Stream 요소의 개수를 구하는 방법은 두 가지가 있습니다.

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
long count = list.stream().filter(i -> i % 2 == 0).count();
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
long count = list.stream().filter(i -> i % 2 == 0).collect(Collectors.counting());

두 개의 코드가 동일한 결과를 도출하지만, 동작하는 방식에는 약간의 차이가 있습니다.

/**
     * Returns the count of elements in this stream.  This is a special case of
     * a <a href="package-summary.html#Reduction">reduction</a> and is
     * equivalent to:
     * <pre>{@code
     *     return mapToLong(e -> 1L).sum();
     * }</pre>
     *
     * <p>This is a <a href="package-summary.html#StreamOps">terminal operation</a>.
     *
     * @apiNote
     * An implementation may choose to not execute the stream pipeline (either
     * sequentially or in parallel) if it is capable of computing the count
     * directly from the stream source.  In such cases no source elements will
     * be traversed and no intermediate operations will be evaluated.
     * Behavioral parameters with side-effects, which are strongly discouraged
     * except for harmless cases such as debugging, may be affected.  For
     * example, consider the following stream:
     * <pre>{@code
     *     List<String> l = Arrays.asList("A", "B", "C", "D");
     *     long count = l.stream().peek(System.out::println).count();
     * }</pre>
     * The number of elements covered by the stream source, a {@code List}, is
     * known and the intermediate operation, {@code peek}, does not inject into
     * or remove elements from the stream (as may be the case for
     * {@code flatMap} or {@code filter} operations).  Thus the count is the
     * size of the {@code List} and there is no need to execute the pipeline
     * and, as a side-effect, print out the list elements.
     *
     * @return the count of elements in this stream
     */
    long count();
    /**
     * Returns a {@code Collector} accepting elements of type {@code T} that
     * counts the number of input elements.  If no elements are present, the
     * result is 0.
     *
     * @implSpec
     * This produces a result equivalent to:
     * <pre>{@code
     *     reducing(0L, e -> 1L, Long::sum)
     * }</pre>
     *
     * @param <T> the type of the input elements
     * @return a {@code Collector} that counts the input elements
     */
    public static <T> Collector<T, ?, Long>
    counting() {
        return summingLong(e -> 1L);
    }

위에 보이는 것처럼 count의 경우에는 모든 요소를 1L로 매핑한 후 최종 연산 sum()을 하는 두 개의 단계를 거치고, counting의 경우에는 매핑과 sum을 한 번에 처리하는 reduce를 활용합니다. 병렬 처리를 하는 경우 counting은 매핑과 연산을 같이 처리하고 내부적으로 병렬 최적화를 하기 때문에 중간 연산을 포함하는 count보다 유리합니다. 

An implementation may choose to not execute the stream pipeline (either sequentially or in parallel) if it is capable of computing the count directly from the stream source.

count 메서드의 주석에는 이런 내용이 있는데, 데이터 원본이 ArrayList와 같이 size를 저장하고 있으면서 filter나 flatMap 같이 스트림 요소 개수에 변화를 주는 중간 연산이 없는 경우에는 요소를 순회하지 않고 바로 개수 정보를 반환하기 때문에 이런 경우에는 count 메서드를 사용하는 것이 counting보다 더 효율적일 수 있습니다.

 

8.2 요약 연산

1. maxBy, minBy

컬렉션에서 최댓값, 최솟값을 가진 요소를 찾는 메서드

import java.util.Arrays;
import java.util.List;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Charlie", 22)
        );

       Optional<Person> oldestPerson = people.stream()
            .collect(Collectors.maxBy(Comparator.comparingInt(Person::getAge)));
    }
}

2. summingInt / averageInt / summarizingInt

요약을 통해 매핑된 값의 합, 평균, 최솟값, 최댓값을 구하는 기능

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Person> people = Arrays.asList(
            new Person("Alice", 25),
            new Person("Bob", 30),
            new Person("Charlie", 22)
        );

        int totalAge = people.stream()
            .collect(Collectors.summingInt(Person::getAge));

        System.out.println("나이의 합: " + totalAge);
        
        OptionalDouble averageAge = people.stream()
            .mapToInt(Person::getAge)
            .average();

        averageAge.ifPresent(avg -> System.out.println("나이의 평균: " + avg));
        
         IntSummaryStatistics ageStats = people.stream()
            .collect(Collectors.summarizingInt(Person::getAge));

        System.out.println("나이의 합: " + ageStats.getSum());
        System.out.println("나이의 평균: " + ageStats.getAverage());
        System.out.println("나이의 최소값: " + ageStats.getMin());
        System.out.println("나이의 최대값: " + ageStats.getMax());
    }
}

 

8.3 문자열 연결

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("가", "나", "다", "라");

        // 공백을 구분자로 사용하여 요소들을 결합
        String result2 = words.stream()
            .collect(Collectors.joining(" "));
        System.out.println(result2); // 가 나 다 라

        // 쉼표와 공백을 구분자로 사용하여 요소들을 결합하고 접두사와 접미사 추가
        String result3 = words.stream()
            .collect(Collectors.joining(", ", "[", "]"));
        System.out.println(result3); // [가, 나, 다, 라]
    }
}

Collectors.joining() 메서드를 이용하면 구분자와 접두사, 접미사를 사용해 요소를 하나의 문자열로 결합할 수 있습니다.