컬렉터

sungs·2025년 9월 2일

자바

목록 보기
89/95

Collectors

스트림을 사용하던 중 통계나 리스트, 컬렉션을 만들고 싶을 때 collect()라는 최종 연산과 함께 쓰인다. 참고로 Collector 인터페이스를 구현한 클래스다.

주요 메서드

toList(), toUnmodifiableList()

스트림을 리스트로 만든다. 후자는 변경할 수 없는 리스트로 만든다.

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("예외 발생: 수정 불가능한 리스트에는 요소를 추가할 수 없습니다.");
        }

toSet(), toCollection(HashSet::new)

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]

toMap(keyMapper, valueMapper, mergeFunction, mapSupplier)

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}

groupingBy(classifier, downstreamCollector)

특정 기준(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()));
        });

partitioningBy(predicate, downstreamCollector)

특정 조건을 만족하는지에 따라 참, 거짓으로 그룹을 나눈다.

 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()

스트림의 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()의 매개변수로 쓰여 각 그룹 별로 특정 작업을 할 수 있게 해준다. 예를 들어 학년 별로 그룹을 나눈 다음 각 학년 별 평균을 낸다든지 말이다. 그러니까 각 그룹 별로 추가 작업을 한다고 생각하면 된다.

다운 스트림 컬렉터에도 다양한 종류가 있다. 기존 컬렉터 메서드들에 이어 추가적인 것들이 있다.

mapping()

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);

collectingAndThen()

이미 나온 최종 결과에 한 번 더 작업을 하는 것이다. 각 요소를 손보는 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("예외 발생: 이 리스트는 수정할 수 없습니다.");
        }
profile
앱 개발 공부 중

0개의 댓글