스트림을 사용하던 중 통계나 리스트, 컬렉션을 만들고 싶을 때 collect()라는 최종 연산과 함께 쓰인다. 참고로 Collector 인터페이스를 구현한 클래스다.
스트림을 리스트로 만든다. 후자는 변경할 수 없는 리스트로 만든다.
Stream<String> stringStream = Stream.of("Apple", "Banana", "Cherry");
// 2. Collectors.toList()를 사용하여 수정 가능한 리스트로 수집
List<String> mutableList = stringStream.collect(Collectors.toList());
// 3. 리스트에 새로운 요소 추가 (가능)
mutableList.add("Durian");
System.out.println("수정 가능한 리스트: " + mutableList);
Stream<String> stringStream = Stream.of("Apple", "Banana", "Cherry");
// 2. Collectors.toUnmodifiableList()를 사용하여 수정 불가능한 리스트로 수집
List<String> unmodifiableList = stringStream.collect(Collectors.toUnmodifiableList());
System.out.println("수정 불가능한 리스트: " + unmodifiableList); // [Apple, Banana, Cherry]
// 3. 리스트에 새로운 요소 추가 시도 (예외 발생)
try {
unmodifiableList.add("Durian");
} catch (UnsupportedOperationException e) {
System.out.println("예외 발생: 수정 불가능한 리스트에는 요소를 추가할 수 없습니다.");
}
Set을 만든다. toCollection을 활용해서 원하는 컬렉션을 만들어 낼 수 있다.
Stream<String> stringStream = Stream.of("Apple", "Banana", "Cherry", "Banana");
// 2. Collectors.toSet()을 사용하여 Set으로 수집
// 중복된 "Banana"는 자동으로 제거된다.
Set<String> fruitSet = stringStream.collect(Collectors.toSet());
// 3. 결과 출력 (순서는 보장되지 않음)
System.out.println("Set으로 수집된 결과: " + fruitSet);
// 출력 예시: [Apple, Cherry, Banana]
Map을 만든다. 매개변수는 각각 키, 값, 중복 합칠 것인지, 맵의 형태다. 중보과 맵 형태는 필수가 아니다.
List<Product> products = List.of(
new Product("Apple", 10),
new Product("Banana", 20),
new Product("Orange", 15),
new Product("Apple", 5), // <-- "Apple" 중복
new Product("Banana", 10) // <-- "Banana" 중복
);
// 2. toMap을 사용하여 중복된 상품의 수량을 합산
// 결과는 LinkedHashMap에 저장하여 입력 순서를 유지한다.
Map<String, Integer> productMap = products.stream()
.collect(Collectors.toMap(
Product::name, // 1. Key: 상품 이름
Product::quantity, // 2. Value: 상품 수량
Integer::sum, // 3. Merge: 중복 키 발생 시, 기존 값과 새 값을 더함
LinkedHashMap::new // 4. Supplier: 결과를 LinkedHashMap으로 생성
));
// 3. 결과 출력
System.out.println("상품별 총 수량 (입력 순서 유지):");
productMap.forEach((name, quantity) ->
System.out.println(name + ": " + quantity)
);
// 전체 맵 출력: {Apple=15, Banana=30, Orange=15}
특정 기준(classifier)에 따라 그룹을 나누는 것이다. downstreamCollector는 그렇게 나뉜 각 그룹별 어떤 작업을 할지 정하는 것이다. 필수는 아니다.
Stream<Fruit> fruits = Stream.of(
new Fruit("사과", "과일", 2000),
new Fruit("바나나", "과일", 1500),
new Fruit("상추", "채소", 1000),
new Fruit("오렌지", "과일", 2500),
new Fruit("시금치", "채소", 1200)
);
// 다운스트림 컬렉터 없이 '종류(type)'로만 그룹화
// 결과: Map<String, List<Fruit>>
Map<String, List<Fruit>> fruitsByType = fruits.stream()
.collect(Collectors.groupingBy(Fruit::type));
fruitsByType.forEach((type, list) -> {
System.out.println("종류: " + type);
list.forEach(fruit -> System.out.println(" - " + fruit.name()));
});
특정 조건을 만족하는지에 따라 참, 거짓으로 그룹을 나눈다.
IntStream numbers = IntStream.rangeClosed(1, 10);
// 2. 숫자가 짝수인지(true) 아닌지(false)에 따라 두 그룹으로 분할
// Predicate(n -> n % 2 == 0)의 결과가 true 또는 false가 됩니다.
Map<Boolean, List<Integer>> numberGroups = numbers
.boxed() // IntStream을 Stream<Integer>로 변환
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
// 3. 결과 출력
System.out.println("짝수 그룹 (true): " + numberGroups.get(true));
System.out.println("홀수 그룹 (false): " + numberGroups.get(false));
counting(), averagingInt(), summingInt() 등을 통해 다양한 통계 관련 정보를 얻을 수 있다. summarizingInt()는 아예 통계 정보를 담은 IntSummaryStatics을 반환하여 get을 통해 정보들을 얻을 수 있다.
maxBy(), minBy()을 이용하여 최댓값과 최솟값도 구할 수 있다. 다만, compareTo를 받아야 한다.
Integer max1 = Stream.of(1, 2, 3)
.collect(Collectors.maxBy(
((i1, i2) -> i1.compareTo(i2)))
).get();
System.out.println("max1 = " + max1);
스트림의 reducing과 같다. 하나의 값으로 줄여준다. 이러한 점을 이용해 최대값을 구할 수도 있다.
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// 1. 초기값(0)을 설정한다.
// 2. 두 숫자를 더하는 연산(Integer::sum)을 지정한다.
Integer sum = numbers.stream()
.collect(Collectors.reducing(0, Integer::sum));
// 동작 과정:
// (0 + 1) -> 1
// (1 + 2) -> 3
// (3 + 3) -> 6
// (6 + 4) -> 10
// (10 + 5) -> 15
System.out.println("숫자의 합: " + sum);
groupingBy()나 partitionBy()의 매개변수로 쓰여 각 그룹 별로 특정 작업을 할 수 있게 해준다. 예를 들어 학년 별로 그룹을 나눈 다음 각 학년 별 평균을 낸다든지 말이다. 그러니까 각 그룹 별로 추가 작업을 한다고 생각하면 된다.
다운 스트림 컬렉터에도 다양한 종류가 있다. 기존 컬렉터 메서드들에 이어 추가적인 것들이 있다.
map처럼 각 요소를 변환시켜준다. 그러고는 변환된 것들을 다른 콜렉터로 수집한다.
List<Product> products = List.of(
new Product("과일", "사과"),
new Product("채소", "상추"),
new Product("과일", "바나나"),
new Product("과일", "오렌지"),
new Product("채소", "시금치")
);
// 2. groupingBy와 mapping을 함께 사용
// - 먼저 카테고리(Product::category)로 그룹화한다.
// - [다운스트림] 각 그룹의 Product 객체에 이름(Product::name)을 추출하는 매핑을 적용한다.
// - 매핑된 이름들을 다시 리스트(toList)로 수집한다.
Map<String, List<String>> namesByCategory = products.stream()
.collect(Collectors.groupingBy(
Product::category,
Collectors.mapping(Product::name, Collectors.toList())
));
// 3. 결과 출력
System.out.println(namesByCategory);
이미 나온 최종 결과에 한 번 더 작업을 하는 것이다. 각 요소를 손보는 mapping과는 다르게 이 메서드는 최종 결과에서 한 번 더 손본다.
Stream<String> fruits = Stream.of("사과", "바나나", "오렌지");
// 2. collectingAndThen을 사용하여 최종 변환 수행
// - 첫 번째 인자(Collectors.toList()): 먼저 리스트로 수집한다.
// - 두 번째 인자(Collections::unmodifiableList): 수집된 리스트를 수정 불가능하게 만든다.
List<String> unmodifiableList = fruits.collect(
Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
)
);
// 3. 결과 출력
System.out.println("결과 리스트: " + unmodifiableList);
// 4. 리스트 수정 시도 (예외 발생)
try {
unmodifiableList.add("포도");
} catch (UnsupportedOperationException e) {
System.out.println("예외 발생: 이 리스트는 수정할 수 없습니다.");
}