14.4 Collectors 심화
Collectors 클래스는 스트림의 collect() 최종 연산에 사용하는 수집 전략을 제공합니다. 기본적인 toList(), joining()을 넘어, 데이터를 그룹화하고 집계하는 강력한 기능들을 익혀봅니다.
1. 기본 수집기 복습
import java.util.*;
import java.util.stream.*;
List<String> fruits = List.of("apple", "banana", "cherry", "apricot", "blueberry");
// toList() - Java 16+ 간단 버전 vs Collectors.toList()
List<String> list1 = fruits.stream().collect(Collectors.toList()); // 수정 가능
List<String> list2 = fruits.stream().toList(); // 불변 (Java 16+)
// toSet()
Set<String> set = fruits.stream().collect(Collectors.toSet());
// joining - 문자열로 연결
String joined = fruits.stream().collect(Collectors.joining(", "));
// "apple, banana, cherry, apricot, blueberry"
String withBrackets = fruits.stream()
.collect(Collectors.joining(", ", "[", "]"));
// "[apple, banana, cherry, apricot, blueberry]"
2. groupingBy - 그룹화
데이터를 특정 기준으로 Map으로 그룹화합니다.
record Person(String name, int age, String city, double salary) {}
List<Person> people = List.of(
new Person("Alice", 28, "서울", 5000),
new Person("Bob", 35, "부산", 6000),
new Person("Charlie", 28, "서울", 7000),
new Person("Dave", 35, "서울", 4500),
new Person("Eve", 22, "대구", 3500)
);
// 도시별 그룹화
Map<String, List<Person>> byCity = people.stream()
.collect(Collectors.groupingBy(Person::city));
// {서울=[Alice, Charlie, Dave], 부산=[Bob], 대구=[Eve]}
byCity.forEach((city, persons) -> {
System.out.println(city + ": " + persons.stream()
.map(Person::name).collect(Collectors.joining(", ")));
});
// 나이별 그룹화 + 이름만 추출 (다운스트림 수집기)
Map<Integer, List<String>> namesByAge = people.stream()
.collect(Collectors.groupingBy(
Person::age, // 분류 기준
Collectors.mapping(Person::name, Collectors.toList()) // 다운스트림
));
System.out.println(namesByAge);
// {22=[Eve], 28=[Alice, Charlie], 35=[Bob, Dave]}
// 그룹별 카운트
Map<String, Long> countByCity = people.stream()
.collect(Collectors.groupingBy(Person::city, Collectors.counting()));
System.out.println(countByCity); // {서울=3, 부산=1, 대구=1}
// 그룹별 평균 급여
Map<String, Double> avgSalaryByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.averagingDouble(Person::salary)));
System.out.println(avgSalaryByCity); // {서울=5500.0, 부산=6000.0, 대구=3500.0}
다중 레벨 그룹화
// 도시별 → 나이별 중첩 그룹화
Map<String, Map<Integer, List<Person>>> byCityAndAge = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.groupingBy(Person::age)));
byCityAndAge.forEach((city, ageMap) -> {
System.out.println("=== " + city + " ===");
ageMap.forEach((age, persons) ->
System.out.println(" " + age + "세: " + persons.stream()
.map(Person::name).collect(Collectors.joining(", "))));
});
3. partitioningBy - 두 그룹으로 분할
boolean 조건으로 true/false 두 그룹으로 분류합니다.
// 고연봉자(5000 이상)와 저연봉자 분리
Map<Boolean, List<Person>> partitioned = people.stream()
.collect(Collectors.partitioningBy(p -> p.salary() >= 5000));
System.out.println("고연봉: " + partitioned.get(true).stream()
.map(Person::name).collect(Collectors.joining(", ")));
// 고연봉: Alice, Bob, Charlie
System.out.println("저연봉: " + partitioned.get(false).stream()
.map(Person::name).collect(Collectors.joining(", ")));
// 저연봉: Dave, Eve
// 수로 분할 (소수 vs 합성수)
List<Integer> nums = IntStream.rangeClosed(2, 20).boxed().toList();
Map<Boolean, List<Integer>> primes = nums.stream()
.collect(Collectors.partitioningBy(n -> {
for (int i = 2; i * i <= n; i++)
if (n % i == 0) return false;
return true;
}));
System.out.println("소수: " + primes.get(true));
// 소수: [2, 3, 5, 7, 11, 13, 17, 19]
4. toMap - Map으로 변환
// toMap(keyMapper, valueMapper)
Map<String, Double> nameSalaryMap = people.stream()
.collect(Collectors.toMap(
Person::name, // 키
Person::salary // 값
));
// {Alice=5000.0, Bob=6000.0, ...}
// 충돌 처리 (같은 키 발생 시)
Map<String, Double> avgSalaryMap = people.stream()
.collect(Collectors.toMap(
Person::city,
Person::salary,
(existing, replacement) -> (existing + replacement) / 2 // 평균
));
// 순서 유지 (LinkedHashMap으로 수집)
Map<String, Double> orderedMap = people.stream()
.collect(Collectors.toMap(
Person::name,
Person::salary,
(a, b) -> a, // 충돌 시 첫 번째 값 유지
LinkedHashMap::new // Map 구현체 지정
));
5. 통계 수집기
// summarizingDouble - 한 번에 여러 통계
DoubleSummaryStatistics stats = people.stream()
.collect(Collectors.summarizingDouble(Person::salary));
System.out.println("개수: " + stats.getCount()); // 5
System.out.println("합계: " + stats.getSum()); // 26000.0
System.out.println("평균: " + stats.getAverage()); // 5200.0
System.out.println("최솟값: " + stats.getMin()); // 3500.0
System.out.println("최댓값: " + stats.getMax()); // 7000.0
// 그룹별 통계
Map<String, DoubleSummaryStatistics> statsByCity = people.stream()
.collect(Collectors.groupingBy(Person::city,
Collectors.summarizingDouble(Person::salary)));
6. 커스텀 Collector
직접 수집기를 만들 수 있습니다 (고급 기능).
// 문자열 리스트를 단어 빈도수 Map으로 수집하는 커스텀 수집기
List<String> words = List.of("hello", "world", "hello", "java", "world", "hello");
// 방법 1: Collectors.toMap + merge 함수
Map<String, Long> wordCount = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
System.out.println(wordCount); // {hello=3, world=2, java=1}
// 방법 2: Collector.of로 커스텀 수집기
Collector<String, Map<String, Integer>, Map<String, Integer>> wordFreqCollector =
Collector.of(
HashMap::new, // supplier
(map, word) -> map.merge(word, 1, Integer::sum), // accumulator
(m1, m2) -> { m1.putAll(m2); return m1; }, // combiner
Collector.Characteristics.IDENTITY_FINISH // characteristics
);
Map<String, Integer> freq = words.stream().collect(wordFreqCollector);
System.out.println(freq); // {hello=3, world=2, java=1}
고수 팁
groupingBy 결과를 정렬된 Map으로 받기:
// TreeMap으로 키를 자동 정렬
Map<String, Long> sorted = people.stream()
.collect(Collectors.groupingBy(
Person::city,
TreeMap::new, // Map 구현체 지정
Collectors.counting()
));
// {대구=1, 부산=1, 서울=3} (사전 순)
Collectors.teeing (Java 12+): 스트림을 두 수집기로 동시에 처리하고 결과를 합칩니다.
// 최솟값과 최댓값을 한 번에
record MinMax(double min, double max) {}
MinMax result = people.stream()
.collect(Collectors.teeing(
Collectors.minBy(Comparator.comparingDouble(Person::salary)),
Collectors.maxBy(Comparator.comparingDouble(Person::salary)),
(min, max) -> new MinMax(
min.map(Person::salary).orElse(0.0),
max.map(Person::salary).orElse(0.0)
)
));
System.out.println(result); // MinMax[min=3500.0, max=7000.0]