3주차 Unit 10.3 — Collectors와 reduce

Psj·2026년 5월 20일

F-lab

목록 보기
119/230

Unit 10.3 — Collectors와 reduce

F-LAB JAVA · 3주차 · Phase 10 · 함수형 프로그래밍
★ 마스터 Unit — Collectors 의 모든 것


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Collector 인터페이스 의 4가지 구성 요소 (supplier, accumulator, combiner, finisher) 는?
  • Collectors 의 기본 수집 (toList, toSet, toMap, toCollection) 은?
  • Collectors 의 문자열 (joining) 은?
  • Collectors 의 집계 (counting, summing, averaging, summarizing) 는?
  • groupingBy 의 정밀 (단순/다운스트림/3-arg) 은?
  • partitioningBy 와 groupingBy 의 차이는?
  • 다운스트림 컬렉터 (mapping, filtering, flatMapping, reducing) 는?
  • reduce 의 3가지 형태 의 차이는?
  • reduce vs collect 의 차이 (immutable vs mutable) 는?
  • 사용자 정의 Collector 만들기는?

🎯 핵심 한 문장

Collectors 는 Stream 의 collect() 최종 연산에 사용하는 다양한 수집 전략을 제공하는 유틸리티 클래스이며, reduce 는 요소들을 하나의 값으로 축약하는 핵심 연산이다.
기본 수집 (toList, toSet, toMap), 문자열 결합 (joining), 집계 (counting, summingInt, averagingDouble, summarizingInt) 가 자주 쓰임.
groupingBy 는 분류 함수로 Map<K, List> 생성, 다운스트림 컬렉터 (counting, mapping, reducing) 와 결합해 강력한 집계 가능.
reduce 는 3가지 형태 — (identity, accumulator), (accumulator) → Optional, (identity, accumulator, combiner) (병렬) — 로 축약.
reduce 는 immutable reduction (각 단계 새 값), collect 는 mutable reduction (가변 컨테이너에 누적) — 성능과 의미가 다름.

비유 — 데이터 분류 작업장

Stream = 컨베이어 벨트의 흐름

Collector = 끝에서 분류하는 작업자
  - toList: 상자에 차곡차곡
  - toSet: 중복 제거하며 상자에
  - toMap: 라벨 붙여 칸에
  - joining: 끈으로 묶기
  - groupingBy: 카테고리별 상자

groupingBy + 다운스트림:
  카테고리별 + 각 카테고리에서 추가 작업
  - "도시별로 나누고, 각 도시 인원 세기"
  - groupingBy(City) + counting()

reduce:
  모든 물건을 하나로 합치기
  - 무게 모두 더하기
  - 가장 무거운 것 찾기

→ Collectors/reduce = Stream 의 최종 처리.


🧭 9개 섹션 로드맵

1. Collector 인터페이스의 구조
2. 기본 수집 (toList, toSet, toMap)
3. 문자열 결합 (joining)
4. 집계 (counting, summing, averaging)
5. groupingBy 의 정밀
6. partitioningBy
7. 다운스트림 컬렉터
8. reduce 의 3가지 형태
9. 면접 + 자기 점검 + Phase 10 중간 시험

1️⃣ Collector 인터페이스의 구조

1.1 Collector 인터페이스

public interface Collector<T, A, R> {
    
    Supplier<A> supplier();              // 1. 컨테이너 생성
    BiConsumer<A, T> accumulator();      // 2. 요소 누적
    BinaryOperator<A> combiner();        // 3. 병렬 병합
    Function<A, R> finisher();           // 4. 최종 변환
    Set<Characteristics> characteristics();   // 특성
    
    // T: 입력 요소 타입
    // A: 누적 컨테이너 타입 (accumulator)
    // R: 최종 결과 타입 (result)
}

1.2 4가지 구성 요소

1. supplier (공급자)
   - 새 컨테이너 생성
   - 예: () -> new ArrayList<>()

2. accumulator (누적자)
   - 컨테이너에 요소 추가
   - 예: (list, item) -> list.add(item)

3. combiner (결합자)
   - 두 컨테이너 병합 (병렬)
   - 예: (list1, list2) -> { list1.addAll(list2); return list1; }

4. finisher (완성자)
   - 컨테이너 → 최종 결과
   - 예: list -> Collections.unmodifiableList(list)

1.3 동작 흐름

collect 의 동작:

순차:
  1. supplier() — 컨테이너 생성
  2. 각 요소마다 accumulator() 호출
  3. finisher() — 최종 변환
  (combiner 사용 X)

병렬:
  1. 각 청크마다 supplier() — 여러 컨테이너
  2. 각 청크에서 accumulator()
  3. combiner() — 컨테이너들 병합
  4. finisher() — 최종 변환

1.4 시각화

// Collectors.toList() 의 개념적 구현
Collector<T, ?, List<T>> toList = Collector.of(
    ArrayList::new,                              // supplier
    List::add,                                    // accumulator
    (left, right) -> { left.addAll(right); return left; },   // combiner
    Collector.Characteristics.IDENTITY_FINISH    // finisher 생략 (그대로)
);

// 순차 실행:
// 1. list = new ArrayList<>()
// 2. for each: list.add(element)
// 3. return list (finisher 없음)

1.5 Characteristics

enum Characteristics {
    CONCURRENT,        // 동시 accumulator 호출 가능
    UNORDERED,         // 순서 무관
    IDENTITY_FINISH    // finisher 가 항등 (생략 가능)
}

// 의미:
// - CONCURRENT: 단일 컨테이너 동시 접근
// - UNORDERED: 결과 순서 무관 (Set 등)
// - IDENTITY_FINISH: A == R (변환 X)

1.6 Collector.of 로 만들기

// 사용자 정의 Collector
Collector<String, StringBuilder, String> concat = Collector.of(
    StringBuilder::new,                        // supplier
    StringBuilder::append,                     // accumulator
    StringBuilder::append,                     // combiner
    StringBuilder::toString                    // finisher
);

String result = Stream.of("A", "B", "C")
    .collect(concat);
// "ABC"

1.7 ILIC 활용

public class CustomCollectorExample {
    
    // 사용자 정의 — 통계 수집
    public Collector<Shipment, ?, ShipmentSummary> toSummary() {
        return Collector.of(
            ShipmentSummary::new,                     // supplier
            ShipmentSummary::accept,                  // accumulator
            ShipmentSummary::merge,                   // combiner
            Function.identity()                       // finisher
        );
    }
    
    static class ShipmentSummary {
        long count = 0;
        BigDecimal totalWeight = BigDecimal.ZERO;
        
        void accept(Shipment s) {
            count++;
            totalWeight = totalWeight.add(s.getWeight());
        }
        
        ShipmentSummary merge(ShipmentSummary other) {
            count += other.count;
            totalWeight = totalWeight.add(other.totalWeight);
            return this;
        }
    }
    
    public ShipmentSummary summarize(List<Shipment> shipments) {
        return shipments.stream().collect(toSummary());
    }
}

1.8 자기 점검 답변

Collector 인터페이스의 4가지 구성 요소는?

:
1. supplier: 컨테이너 생성
2. accumulator: 요소 누적
3. combiner: 병렬 병합
4. finisher: 최종 변환

+ characteristics: CONCURRENT, UNORDERED, IDENTITY_FINISH

동작:

  • 순차: supplier → accumulator → finisher
  • 병렬: 각 청크 + combiner

2️⃣ 기본 수집 (toList, toSet, toMap)

2.1 toList

// Collectors.toList — 가변 List (보통 ArrayList)
List<String> list = stream.collect(Collectors.toList());

// vs Stream.toList (Java 16+) — 불변
List<String> immutable = stream.toList();

// 차이:
// - Collectors.toList: 가변 (add 가능)
// - Stream.toList: 불변

// 불변 명시
List<String> unmod = stream.collect(Collectors.toUnmodifiableList());

2.2 toSet

// 중복 제거 + Set
Set<String> set = stream.collect(Collectors.toSet());
// 보통 HashSet

// 불변
Set<String> unmodSet = stream.collect(Collectors.toUnmodifiableSet());

// 특정 Set
TreeSet<String> treeSet = stream.collect(
    Collectors.toCollection(TreeSet::new));

2.3 toMap

// 키 함수 + 값 함수
Map<K, V> toMap(
    Function<? super T, ? extends K> keyMapper,
    Function<? super T, ? extends V> valueMapper);

// 활용
Map<Long, Shipment> byId = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getId,    // 키
        s -> s              // 값
    ));

// 키만 추출하고 객체를 값으로
Map<String, Shipment> byBlNo = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getBlNo,
        Function.identity()
    ));

2.4 toMap — 중복 키 처리

// 중복 키 발생 시 IllegalStateException
Map<String, Shipment> map = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getStatus,   // 중복 가능 키!
        Function.identity()
    ));
// "Duplicate key" 예외

// 해결: merge 함수 (3번째 인자)
Map<String, Shipment> map2 = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getStatus,
        Function.identity(),
        (existing, replacement) -> existing   // 기존 유지
    ));

// 또는 새 것으로
(existing, replacement) -> replacement

// 또는 합치기
(s1, s2) -> s1.merge(s2)

2.5 toMap — Map 종류 지정

// 4번째 인자: Map 공급자
TreeMap<String, Shipment> sorted = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getBlNo,
        Function.identity(),
        (a, b) -> a,            // merge
        TreeMap::new            // Map 종류
    ));

// LinkedHashMap (순서 유지)
LinkedHashMap<String, Shipment> ordered = shipments.stream()
    .collect(Collectors.toMap(
        Shipment::getBlNo,
        Function.identity(),
        (a, b) -> a,
        LinkedHashMap::new
    ));

2.6 toCollection

// 특정 컬렉션 지정
ArrayList<String> arrayList = stream.collect(
    Collectors.toCollection(ArrayList::new));

LinkedList<String> linkedList = stream.collect(
    Collectors.toCollection(LinkedList::new));

TreeSet<String> treeSet = stream.collect(
    Collectors.toCollection(TreeSet::new));

// 정렬된 Set
TreeSet<Shipment> sorted = shipments.stream()
    .collect(Collectors.toCollection(() -> 
        new TreeSet<>(Comparator.comparing(Shipment::getWeight))));

2.7 비교

Collector결과가변?
toList()List (ArrayList)가변
toUnmodifiableList()List불변
toSet()Set (HashSet)가변
toUnmodifiableSet()Set불변
toMap(k, v)Map (HashMap)가변
toCollection(supplier)지정 컬렉션가변

2.8 ILIC 활용

public class ShipmentCollectors {
    
    // 1. List
    public List<String> getBlNos() {
        return shipments.stream()
            .map(Shipment::getBlNo)
            .collect(Collectors.toList());
    }
    
    // 2. Set (중복 제거)
    public Set<String> getUniqueStatuses() {
        return shipments.stream()
            .map(Shipment::getStatus)
            .collect(Collectors.toSet());
    }
    
    // 3. Map (ID → Shipment)
    public Map<Long, Shipment> indexById() {
        return shipments.stream()
            .collect(Collectors.toMap(
                Shipment::getId,
                Function.identity()
            ));
    }
    
    // 4. Map (중복 키 처리)
    public Map<String, Shipment> latestByBlNo() {
        return shipments.stream()
            .collect(Collectors.toMap(
                Shipment::getBlNo,
                Function.identity(),
                (older, newer) -> 
                    newer.getCreatedAt().isAfter(older.getCreatedAt()) ? newer : older
            ));
    }
    
    // 5. 정렬된 Set
    public TreeSet<Shipment> byWeight() {
        return shipments.stream()
            .collect(Collectors.toCollection(() ->
                new TreeSet<>(Comparator.comparing(Shipment::getWeight))));
    }
}

2.9 자기 점검 답변

기본 수집 컬렉터는?

:
1. toList():

  • 가변 List
  • vs Stream.toList() (불변)
  1. toSet():

    • 중복 제거
    • HashSet
  2. toMap(k, v):

    • 키/값 함수
    • 중복 키 → 3번째 merge 함수
    • 4번째 Map 공급자
  3. toCollection(supplier):

    • 특정 컬렉션
    • TreeSet, LinkedList 등
  4. 불변 버전:

    • toUnmodifiableList/Set/Map

3️⃣ 문자열 결합 (joining)

3.1 joining

// 1. 단순 결합
String joining();

// 2. 구분자
String joining(CharSequence delimiter);

// 3. 구분자 + 접두/접미
String joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix);

// 활용
String s1 = Stream.of("A", "B", "C")
    .collect(Collectors.joining());
// "ABC"

String s2 = Stream.of("A", "B", "C")
    .collect(Collectors.joining(", "));
// "A, B, C"

String s3 = Stream.of("A", "B", "C")
    .collect(Collectors.joining(", ", "[", "]"));
// "[A, B, C]"

3.2 객체 결합

// 객체 → 문자열 → 결합
String blNos = shipments.stream()
    .map(Shipment::getBlNo)
    .collect(Collectors.joining(", "));
// "BL-001, BL-002, BL-003"

// CSV 라인
String csvLine = shipments.stream()
    .map(s -> s.getId() + "," + s.getBlNo() + "," + s.getWeight())
    .collect(Collectors.joining("\n"));

// JSON 배열 (간단)
String json = shipments.stream()
    .map(s -> "\"" + s.getBlNo() + "\"")
    .collect(Collectors.joining(", ", "[", "]"));
// ["BL-001", "BL-002"]

3.3 joining vs String.join

// String.join (Java 8+)
String s1 = String.join(", ", List.of("A", "B", "C"));
// "A, B, C"

// Collectors.joining (Stream)
String s2 = Stream.of("A", "B", "C")
    .collect(Collectors.joining(", "));

// 차이:
// - String.join: 간단, 컬렉션/배열 직접
// - Collectors.joining: Stream 파이프라인 중

// 변환 필요 시 Collectors.joining
String s3 = shipments.stream()
    .map(Shipment::getBlNo)
    .collect(Collectors.joining(", "));
// String.join 으로는:
String s4 = String.join(", ",
    shipments.stream().map(Shipment::getBlNo).toList());

3.4 ILIC 활용

public class ShipmentJoining {
    
    // 1. BL 번호 목록
    public String blNoList() {
        return shipments.stream()
            .map(Shipment::getBlNo)
            .collect(Collectors.joining(", "));
    }
    
    // 2. 요약 메시지
    public String summary() {
        return shipments.stream()
            .map(s -> String.format("%s(%.1fkg)", s.getBlNo(), s.getWeight()))
            .collect(Collectors.joining(", ", "Shipments: ", "."));
        // "Shipments: BL-001(100.0kg), BL-002(200.0kg)."
    }
    
    // 3. CSV
    public String toCsv() {
        String header = "ID,BL,Weight";
        String rows = shipments.stream()
            .map(s -> s.getId() + "," + s.getBlNo() + "," + s.getWeight())
            .collect(Collectors.joining("\n"));
        return header + "\n" + rows;
    }
    
    // 4. SQL IN 절
    public String toSqlInClause() {
        return shipments.stream()
            .map(s -> "'" + s.getBlNo() + "'")
            .collect(Collectors.joining(", ", "(", ")"));
        // "('BL-001', 'BL-002')"
    }
}

3.5 자기 점검 답변

joining 의 사용은?

:
1. 3가지 형태:

  • joining(): 단순 결합
  • joining(delimiter): 구분자
  • joining(delimiter, prefix, suffix): 접두/접미
  1. 활용:

    • 객체 → map → joining
    • CSV, JSON, SQL IN
  2. vs String.join:

    • String.join: 컬렉션 직접
    • Collectors.joining: Stream 중

4️⃣ 집계 (counting, summing, averaging)

4.1 counting

// 개수 세기
long count = shipments.stream()
    .collect(Collectors.counting());
// = shipments.stream().count();

// groupingBy 와 함께 (강력)
Map<String, Long> countByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.counting()
    ));
// {PENDING=5, SHIPPED=3, DELIVERED=10}

4.2 summing

// 합계
Collectors.summingInt(ToIntFunction);
Collectors.summingLong(ToLongFunction);
Collectors.summingDouble(ToDoubleFunction);

// 활용
int totalQty = shipments.stream()
    .collect(Collectors.summingInt(Shipment::getQuantity));

double totalWeight = shipments.stream()
    .collect(Collectors.summingDouble(s -> s.getWeight().doubleValue()));

// groupingBy 와 함께
Map<String, Double> weightByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.summingDouble(s -> s.getWeight().doubleValue())
    ));

4.3 averaging

// 평균
Collectors.averagingInt(ToIntFunction);
Collectors.averagingLong(ToLongFunction);
Collectors.averagingDouble(ToDoubleFunction);

// 활용
Double avgWeight = shipments.stream()
    .collect(Collectors.averagingDouble(s -> s.getWeight().doubleValue()));

// 결과는 Double (요소 없으면 0.0)

// groupingBy 와 함께
Map<String, Double> avgByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.averagingDouble(s -> s.getWeight().doubleValue())
    ));

4.4 summarizing

// 통계 한 번에 (count, sum, min, max, average)
Collectors.summarizingInt(ToIntFunction);
Collectors.summarizingLong(ToLongFunction);
Collectors.summarizingDouble(ToDoubleFunction);

// 활용
IntSummaryStatistics stats = shipments.stream()
    .collect(Collectors.summarizingInt(Shipment::getQuantity));

stats.getCount();     // 개수
stats.getSum();       // 합계
stats.getMin();       // 최소
stats.getMax();       // 최대
stats.getAverage();   // 평균

// 한 번에 모든 통계
DoubleSummaryStatistics weightStats = shipments.stream()
    .collect(Collectors.summarizingDouble(s -> s.getWeight().doubleValue()));

4.5 minBy, maxBy

// 최소/최대 (Comparator)
Optional<Shipment> heaviest = shipments.stream()
    .collect(Collectors.maxBy(Comparator.comparing(Shipment::getWeight)));

Optional<Shipment> lightest = shipments.stream()
    .collect(Collectors.minBy(Comparator.comparing(Shipment::getWeight)));

// groupingBy 와 함께
Map<String, Optional<Shipment>> heaviestByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.maxBy(Comparator.comparing(Shipment::getWeight))
    ));

4.6 reducing

// 컬렉터로서의 reduce
Collectors.reducing(BinaryOperator);
Collectors.reducing(identity, BinaryOperator);
Collectors.reducing(identity, mapper, BinaryOperator);

// 활용
Optional<BigDecimal> totalWeight = shipments.stream()
    .collect(Collectors.reducing(
        (a, b) -> a   // 임의 예시
    ));

// 더 일반적 — groupingBy 와 함께
Map<String, BigDecimal> weightByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.reducing(
            BigDecimal.ZERO,
            Shipment::getWeight,
            BigDecimal::add
        )
    ));

4.7 집계 비교

Collector결과의미
counting()Long개수
summingInt/Long/DoubleInteger/Long/Double합계
averagingInt/Long/DoubleDouble평균
summarizingInt/Long/DoubleXxxSummaryStatistics모든 통계
minBy/maxByOptional최소/최대
reducingT 또는 Optional축약

4.8 ILIC 활용

public class ShipmentAggregation {
    
    // 1. 개수
    public long count() {
        return shipments.stream()
            .collect(Collectors.counting());
    }
    
    // 2. 합계
    public double totalWeight() {
        return shipments.stream()
            .collect(Collectors.summingDouble(s -> s.getWeight().doubleValue()));
    }
    
    // 3. 평균
    public double averageWeight() {
        return shipments.stream()
            .collect(Collectors.averagingDouble(s -> s.getWeight().doubleValue()));
    }
    
    // 4. 통계 한 번에
    public DoubleSummaryStatistics weightStatistics() {
        return shipments.stream()
            .collect(Collectors.summarizingDouble(s -> s.getWeight().doubleValue()));
    }
    
    // 5. 최대
    public Optional<Shipment> heaviest() {
        return shipments.stream()
            .collect(Collectors.maxBy(Comparator.comparing(Shipment::getWeight)));
    }
    
    // 6. 상태별 통계 (groupingBy + summarizing)
    public Map<String, DoubleSummaryStatistics> statsByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.summarizingDouble(s -> s.getWeight().doubleValue())
            ));
    }
}

4.9 자기 점검 답변

집계 컬렉터는?

:
1. counting(): 개수 (Long)
2. summingInt/Long/Double: 합계
3. averagingInt/Long/Double: 평균 (Double)
4. summarizingInt/Long/Double: 모든 통계
5. minBy/maxBy: Optional
6. reducing: 축약

활용:

  • 단독 사용
  • groupingBy 의 다운스트림

5️⃣ groupingBy 의 정밀

5.1 단순 groupingBy

// 분류 함수로 그룹핑
Map<K, List<T>> groupingBy(Function<T, K> classifier);

// 활용
Map<String, List<Shipment>> byStatus = shipments.stream()
    .collect(Collectors.groupingBy(Shipment::getStatus));
// {
//   "PENDING": [s1, s3, s5],
//   "SHIPPED": [s2, s4],
//   "DELIVERED": [s6]
// }

5.2 groupingBy + 다운스트림

// 분류 + 각 그룹에 추가 컬렉터
Map<K, D> groupingBy(
    Function<T, K> classifier,
    Collector<T, A, D> downstream);

// 활용 — 그룹별 개수
Map<String, Long> countByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.counting()
    ));
// {PENDING=3, SHIPPED=2, DELIVERED=1}

// 그룹별 합계
Map<String, Double> weightByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.summingDouble(s -> s.getWeight().doubleValue())
    ));

// 그룹별 BL 번호 목록
Map<String, List<String>> blNosByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.mapping(Shipment::getBlNo, Collectors.toList())
    ));

5.3 groupingBy 3-arg (Map 종류 지정)

// 분류 + Map 공급자 + 다운스트림
Map<K, D> groupingBy(
    Function<T, K> classifier,
    Supplier<Map<K, D>> mapFactory,
    Collector<T, A, D> downstream);

// 활용 — TreeMap 으로 정렬
TreeMap<String, Long> sortedCount = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        TreeMap::new,                  // Map 종류
        Collectors.counting()
    ));
// 키 정렬됨

// LinkedHashMap (순서 유지)
LinkedHashMap<String, List<Shipment>> ordered = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        LinkedHashMap::new,
        Collectors.toList()
    ));

5.4 다단계 groupingBy

// 그룹 안에 그룹 (중첩)
Map<String, Map<Boolean, List<Shipment>>> nested = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,                          // 1차: 상태
        Collectors.groupingBy(Shipment::isUrgent)     // 2차: 긴급 여부
    ));
// {
//   "PENDING": {true: [...], false: [...]},
//   "SHIPPED": {true: [...], false: [...]}
// }

// 3단계도 가능
Map<String, Map<Boolean, Long>> threeLevel = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.groupingBy(
            Shipment::isUrgent,
            Collectors.counting()
        )
    ));

5.5 groupingByConcurrent

// 병렬 + 동시성 Map
ConcurrentMap<String, List<Shipment>> concurrent = shipments.parallelStream()
    .collect(Collectors.groupingByConcurrent(Shipment::getStatus));

// 차이:
// - groupingBy: 순차 또는 병렬 (병합)
// - groupingByConcurrent: ConcurrentHashMap, 진짜 동시 누적
// - 큰 병렬 데이터에서 효율

5.6 활용 시나리오

// 1. 카테고리별 분류
Map<String, List<Product>> byCategory = products.stream()
    .collect(Collectors.groupingBy(Product::getCategory));

// 2. 카테고리별 개수
Map<String, Long> countByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.counting()
    ));

// 3. 카테고리별 평균 가격
Map<String, Double> avgPriceByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.averagingDouble(Product::getPrice)
    ));

// 4. 카테고리별 최고가 상품
Map<String, Optional<Product>> mostExpensive = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.maxBy(Comparator.comparing(Product::getPrice))
    ));

// 5. 카테고리별 상품명 목록
Map<String, List<String>> namesByCategory = products.stream()
    .collect(Collectors.groupingBy(
        Product::getCategory,
        Collectors.mapping(Product::getName, Collectors.toList())
    ));

5.7 ILIC 활용

public class ShipmentGrouping {
    
    // 1. 상태별 분류
    public Map<String, List<Shipment>> byStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(Shipment::getStatus));
    }
    
    // 2. 상태별 개수
    public Map<String, Long> countByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.counting()
            ));
    }
    
    // 3. 상태별 + 긴급 여부 (다단계)
    public Map<String, Map<Boolean, List<Shipment>>> byStatusAndUrgency() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.groupingBy(Shipment::isUrgent)
            ));
    }
    
    // 4. 월별 총 중량 (TreeMap)
    public TreeMap<YearMonth, Double> monthlyWeight() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                s -> YearMonth.from(s.getCreatedAt()),
                TreeMap::new,
                Collectors.summingDouble(s -> s.getWeight().doubleValue())
            ));
    }
    
    // 5. 상태별 BL 번호 목록
    public Map<String, List<String>> blNosByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.mapping(
                    Shipment::getBlNo,
                    Collectors.toList()
                )
            ));
    }
}

5.8 자기 점검 답변

groupingBy 의 정밀은?

:
1. 단순:

  • groupingBy(classifier)
  • Map<K, List>
  1. 다운스트림:

    • groupingBy(classifier, downstream)
    • 각 그룹에 추가 컬렉터
    • counting, summing, mapping 등
  2. 3-arg:

    • groupingBy(classifier, mapFactory, downstream)
    • TreeMap, LinkedHashMap 지정
  3. 다단계:

    • groupingBy 안에 groupingBy
    • 중첩 Map
  4. groupingByConcurrent:

    • ConcurrentHashMap
    • 병렬 효율

6️⃣ partitioningBy

6.1 partitioningBy 의 정의

// Predicate 로 두 그룹 (true/false)
Map<Boolean, List<T>> partitioningBy(Predicate<T> predicate);

// 활용
Map<Boolean, List<Shipment>> partitioned = shipments.stream()
    .collect(Collectors.partitioningBy(Shipment::isUrgent));

List<Shipment> urgent = partitioned.get(true);
List<Shipment> normal = partitioned.get(false);

6.2 groupingBy vs partitioningBy

groupingBy:
  - 분류 함수 (Function)
  - 여러 그룹 (키마다)
  - 없는 키는 Map 에 없음

partitioningBy:
  - 술어 (Predicate)
  - 정확히 2그룹 (true, false)
  - 둘 다 항상 존재 (빈 리스트라도)

차이 예:
  groupingBy(s -> s.isUrgent() ? "urgent" : "normal")
  → {"urgent": [...], "normal": [...]}
  → urgent 없으면 키 없음

  partitioningBy(Shipment::isUrgent)
  → {true: [...], false: [...]}
  → urgent 없어도 {true: [], false: [...]}

6.3 partitioningBy + 다운스트림

// 다운스트림 컬렉터
Map<Boolean, D> partitioningBy(
    Predicate<T> predicate,
    Collector<T, A, D> downstream);

// 활용 — 긴급/일반 개수
Map<Boolean, Long> countByUrgency = shipments.stream()
    .collect(Collectors.partitioningBy(
        Shipment::isUrgent,
        Collectors.counting()
    ));
// {true=3, false=7}

// 긴급/일반 총 중량
Map<Boolean, Double> weightByUrgency = shipments.stream()
    .collect(Collectors.partitioningBy(
        Shipment::isUrgent,
        Collectors.summingDouble(s -> s.getWeight().doubleValue())
    ));

6.4 활용 시나리오

// 1. 합격/불합격 분리
Map<Boolean, List<Student>> passFail = students.stream()
    .collect(Collectors.partitioningBy(s -> s.getScore() >= 60));

List<Student> passed = passFail.get(true);
List<Student> failed = passFail.get(false);

// 2. 짝수/홀수
Map<Boolean, List<Integer>> evenOdd = numbers.stream()
    .collect(Collectors.partitioningBy(n -> n % 2 == 0));

// 3. 합격/불합격 평균
Map<Boolean, Double> avgScores = students.stream()
    .collect(Collectors.partitioningBy(
        s -> s.getScore() >= 60,
        Collectors.averagingDouble(Student::getScore)
    ));

6.5 ILIC 활용

public class ShipmentPartitioning {
    
    // 1. 긴급/일반 분리
    public Map<Boolean, List<Shipment>> byUrgency() {
        return shipments.stream()
            .collect(Collectors.partitioningBy(Shipment::isUrgent));
    }
    
    // 2. 무거운/가벼운 + 개수
    public Map<Boolean, Long> byWeightThreshold() {
        return shipments.stream()
            .collect(Collectors.partitioningBy(
                s -> s.getWeight().compareTo(BigDecimal.valueOf(1000)) > 0,
                Collectors.counting()
            ));
    }
    
    // 3. 유효/무효 분리
    public ValidationGroups validate() {
        Map<Boolean, List<Shipment>> result = shipments.stream()
            .collect(Collectors.partitioningBy(this::isValid));
        
        return new ValidationGroups(
            result.get(true),    // 유효
            result.get(false)    // 무효
        );
    }
    
    private boolean isValid(Shipment s) {
        return s.getWeight() != null 
            && s.getWeight().compareTo(BigDecimal.ZERO) > 0
            && s.getBlNo() != null;
    }
    
    record ValidationGroups(List<Shipment> valid, List<Shipment> invalid) {}
}

6.6 자기 점검 답변

partitioningBy 와 groupingBy 의 차이는?

:
1. partitioningBy:

  • Predicate (boolean)
  • 정확히 2그룹 (true, false)
  • 둘 다 항상 존재
  1. groupingBy:

    • Function (K)
    • 여러 그룹
    • 없는 키는 Map 에 없음
  2. 공통:

    • 다운스트림 컬렉터 가능
  3. 활용:

    • partitioningBy: 이분법
    • groupingBy: 다분류

7️⃣ 다운스트림 컬렉터

7.1 다운스트림 컬렉터란

다운스트림 컬렉터:

  groupingBy / partitioningBy 의 각 그룹에
  추가로 적용하는 컬렉터.

종류:
  - counting
  - summing/averaging/summarizing
  - mapping
  - filtering (Java 9+)
  - flatMapping (Java 9+)
  - reducing
  - collectingAndThen
  - toList, toSet

7.2 mapping

// 각 요소를 변환 후 수집
Collectors.mapping(mapper, downstream);

// 활용 — 그룹별 BL 번호 목록
Map<String, List<String>> blNosByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.mapping(
            Shipment::getBlNo,     // 변환
            Collectors.toList()     // 수집
        )
    ));
// {PENDING: [BL-001, BL-003], SHIPPED: [BL-002]}

// 그룹별 BL 번호 Set
Map<String, Set<String>> blNoSets = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.mapping(Shipment::getBlNo, Collectors.toSet())
    ));

7.3 filtering (Java 9+)

// 그룹 내 필터링
Collectors.filtering(predicate, downstream);

// 활용 — 그룹별 무거운 것만
Map<String, List<Shipment>> heavyByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.filtering(
            s -> s.getWeight().compareTo(BigDecimal.valueOf(1000)) > 0,
            Collectors.toList()
        )
    ));

// 차이 — filter vs filtering:
// .filter() 후 groupingBy: 빈 그룹 사라짐
shipments.stream()
    .filter(s -> s.getWeight().compareTo(BigDecimal.valueOf(1000)) > 0)
    .collect(Collectors.groupingBy(Shipment::getStatus));
// 무거운 게 없는 상태는 키 없음

// filtering: 빈 그룹 유지
shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.filtering(s -> ..., Collectors.toList())
    ));
// 모든 상태 키 유지 (빈 리스트라도)

7.4 flatMapping (Java 9+)

// 그룹 내 평탄화
Collectors.flatMapping(mapper, downstream);

// 활용 — 그룹별 모든 아이템
Map<String, List<ShipmentItem>> itemsByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.flatMapping(
            s -> s.getItems().stream(),   // Stream<ShipmentItem>
            Collectors.toList()
        )
    ));
// 각 상태의 모든 아이템 평탄화

7.5 reducing

// 그룹 내 축약
Collectors.reducing(identity, mapper, op);

// 활용 — 그룹별 총 중량
Map<String, BigDecimal> weightByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.reducing(
            BigDecimal.ZERO,
            Shipment::getWeight,
            BigDecimal::add
        )
    ));

7.6 collectingAndThen

// 수집 후 변환
Collectors.collectingAndThen(downstream, finisher);

// 활용 — 불변 List
List<Shipment> immutable = shipments.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.toList(),
        Collections::unmodifiableList
    ));

// 그룹별 개수를 정수로
Map<String, Integer> countByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.collectingAndThen(
            Collectors.counting(),
            Long::intValue   // Long → Integer
        )
    ));

// 최댓값을 직접 (Optional 해제)
Map<String, Shipment> heaviestByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.collectingAndThen(
            Collectors.maxBy(Comparator.comparing(Shipment::getWeight)),
            Optional::get   // Optional<Shipment> → Shipment
        )
    ));

7.7 다운스트림 조합

// 복잡한 조합
Map<String, Map<Boolean, Long>> complex = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,                       // 1차 그룹
        Collectors.groupingBy(                     // 2차 그룹
            Shipment::isUrgent,
            Collectors.counting()                  // 개수
        )
    ));

// 그룹별 BL 번호를 ", " 로 결합
Map<String, String> joinedByStatus = shipments.stream()
    .collect(Collectors.groupingBy(
        Shipment::getStatus,
        Collectors.mapping(
            Shipment::getBlNo,
            Collectors.joining(", ")
        )
    ));

7.8 ILIC 활용

public class ShipmentDownstream {
    
    // 1. 상태별 BL 번호 목록
    public Map<String, List<String>> blNosByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.mapping(Shipment::getBlNo, Collectors.toList())
            ));
    }
    
    // 2. 상태별 긴급 건만
    public Map<String, List<Shipment>> urgentByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.filtering(Shipment::isUrgent, Collectors.toList())
            ));
    }
    
    // 3. 상태별 모든 아이템
    public Map<String, List<ShipmentItem>> itemsByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.flatMapping(
                    s -> s.getItems().stream(),
                    Collectors.toList()
                )
            ));
    }
    
    // 4. 상태별 최대 중량 Shipment (Optional 해제)
    public Map<String, Shipment> heaviestByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.collectingAndThen(
                    Collectors.maxBy(Comparator.comparing(Shipment::getWeight)),
                    Optional::get
                )
            ));
    }
    
    // 5. 상태별 BL 번호 결합
    public Map<String, String> blNoSummaryByStatus() {
        return shipments.stream()
            .collect(Collectors.groupingBy(
                Shipment::getStatus,
                Collectors.mapping(
                    Shipment::getBlNo,
                    Collectors.joining(", ", "[", "]")
                )
            ));
    }
}

7.9 자기 점검 답변

다운스트림 컬렉터는?

:
1. 종류:

  • mapping: 변환 후 수집
  • filtering (Java 9+): 그룹 내 필터
  • flatMapping (Java 9+): 평탄화
  • reducing: 축약
  • collectingAndThen: 수집 후 변환
  1. mapping vs filter:

    • filter 후 groupingBy: 빈 그룹 사라짐
    • filtering: 빈 그룹 유지
  2. collectingAndThen:

    • Optional 해제
    • 타입 변환
    • 불변 변환
  3. 조합:

    • groupingBy 중첩
    • 복잡한 집계

8️⃣ reduce 의 3가지 형태

8.1 형태 1 — identity + accumulator

T reduce(T identity, BinaryOperator<T> accumulator);

// identity: 초기값 (항등원)
// accumulator: 누적 함수

// 활용
int sum = numbers.stream()
    .reduce(0, Integer::sum);
// 0 + n1 + n2 + ...

int sum2 = numbers.stream()
    .reduce(0, (a, b) -> a + b);

String concat = words.stream()
    .reduce("", String::concat);

BigDecimal total = shipments.stream()
    .map(Shipment::getWeight)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

// identity 의 역할:
// - 빈 Stream 시 반환값
// - 초기 누적값

8.2 형태 2 — accumulator (Optional)

Optional<T> reduce(BinaryOperator<T> accumulator);

// identity 없음
// 빈 Stream 시 Optional.empty()

// 활용
Optional<Integer> max = numbers.stream()
    .reduce(Integer::max);

Optional<Integer> sum = numbers.stream()
    .reduce(Integer::sum);
// 빈 Stream → Optional.empty()
// 요소 1개 → 그 요소
// 요소 2+ → 누적

// 처리
max.ifPresent(System.out::println);
int result = max.orElse(0);

8.3 형태 3 — identity + accumulator + combiner

<U> U reduce(U identity, 
             BiFunction<U, ? super T, U> accumulator,
             BinaryOperator<U> combiner);

// identity: 초기값 (U 타입)
// accumulator: U + T → U (다른 타입 누적)
// combiner: U + U → U (병렬 병합)

// 활용 — 다른 타입으로 축약
int totalLength = words.stream()
    .reduce(0,
        (sum, word) -> sum + word.length(),   // accumulator
        Integer::sum                            // combiner
    );

// 병렬에서 combiner 필수
int parallelLen = words.parallelStream()
    .reduce(0,
        (sum, word) -> sum + word.length(),
        Integer::sum
    );

8.4 세 형태 비교

형태시그니처반환활용
1reduce(identity, acc)T같은 타입 축약
2reduce(acc)Optionalidentity 없음
3reduce(identity, acc, combiner)U다른 타입 + 병렬

8.5 reduce vs collect

reduce (immutable reduction):
  - 각 단계 새 객체
  - 불변
  - String 합치기 시 비효율 (매번 새 String)

collect (mutable reduction):
  - 가변 컨테이너에 누적
  - 효율적 (객체 재사용)
  - StringBuilder 등

예: 문자열 합치기
// ❌ reduce — 비효율
String s1 = words.stream()
    .reduce("", String::concat);
// 매번 새 String 생성 (O(n²))

// ✓ collect — 효율
String s2 = words.stream()
    .collect(Collectors.joining());
// StringBuilder 활용 (O(n))

8.6 reduce 의 항등원 (identity)

// identity 의 조건:
// combiner(identity, x) == x

// 합계: 0
numbers.stream().reduce(0, Integer::sum);   // 0 + x = x ✓

// 곱: 1
numbers.stream().reduce(1, (a, b) -> a * b);   // 1 * x = x ✓

// 최대: Integer.MIN_VALUE
numbers.stream().reduce(Integer.MIN_VALUE, Integer::max);

// 문자열: ""
words.stream().reduce("", String::concat);   // "" + x = x ✓

// 잘못된 identity:
numbers.stream().reduce(10, Integer::sum);
// 10 + x ≠ x
// 결과: 10 + 모든 합 (잘못된 결과)
// 특히 병렬에서 문제

8.7 병렬 reduce 의 주의

// 병렬에서 잘못된 identity
int wrong = numbers.parallelStream()
    .reduce(10, Integer::sum);
// 각 청크마다 10 더해짐
// 4 청크면 10 × 4 = 40 추가
// 잘못된 결과

// 올바른 identity (0)
int correct = numbers.parallelStream()
    .reduce(0, Integer::sum);
// 각 청크 0 + 부분합
// combiner 가 부분합 더함
// 정확

// accumulator 와 combiner 일관성:
// 형태 3 에서 둘이 호환되어야

8.8 ILIC 활용

public class ShipmentReduce {
    
    // 1. 총 중량 (형태 1)
    public BigDecimal totalWeight() {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
    
    // 2. 최대 중량 (형태 2)
    public Optional<BigDecimal> maxWeight() {
        return shipments.stream()
            .map(Shipment::getWeight)
            .reduce(BigDecimal::max);
    }
    
    // 3. 총 아이템 수 (형태 3 — 다른 타입)
    public int totalItemCount() {
        return shipments.stream()
            .reduce(0,
                (sum, s) -> sum + s.getItems().size(),   // accumulator
                Integer::sum                              // combiner
            );
    }
    
    // 4. 가장 무거운 Shipment
    public Optional<Shipment> heaviest() {
        return shipments.stream()
            .reduce((a, b) -> 
                a.getWeight().compareTo(b.getWeight()) > 0 ? a : b);
    }
    
    // 5. reduce vs collect (문자열)
    public String blNoListEfficient() {
        // ✓ collect (효율)
        return shipments.stream()
            .map(Shipment::getBlNo)
            .collect(Collectors.joining(", "));
        // reduce 보다 효율적
    }
}

8.9 자기 점검 답변

reduce 의 3가지 형태는?

:
1. 형태 1:

  • reduce(identity, accumulator)
  • T 반환
  • 같은 타입 축약
  1. 형태 2:

    • reduce(accumulator)
    • Optional 반환
    • identity 없음
  2. 형태 3:

    • reduce(identity, accumulator, combiner)
    • U 반환 (다른 타입)
    • 병렬에서 combiner
  3. reduce vs collect:

    • reduce: immutable (각 단계 새 객체)
    • collect: mutable (가변 컨테이너)
    • 문자열 등은 collect 권장
  4. identity 조건:

    • combiner(identity, x) == x
    • 병렬에서 특히 중요

9️⃣ 면접 + 자기 점검 + Phase 10 중간 시험

9.1 면접 단골 질문 매핑

Q핵심 답변
Collector 의 구성 요소?supplier, accumulator, combiner, finisher
toList vs Stream.toList?가변 vs 불변
toMap 중복 키?3번째 merge 함수
joining?결합 (구분자/접두/접미)
counting/summing/averaging?개수/합계/평균
summarizing?모든 통계 (count/sum/min/max/avg)
groupingBy?Map<K, List>
다운스트림 컬렉터?각 그룹에 추가 컬렉터
partitioningBy vs groupingBy?2그룹 (boolean) vs 다그룹
mapping vs filter?filtering 은 빈 그룹 유지
reduce 3형태?identity/Optional/combiner
reduce vs collect?immutable vs mutable
collectingAndThen?수집 후 변환

9.2 자기 점검 체크리스트

Collector 구조

  • 4가지 구성 요소
  • 동작 흐름
  • Characteristics

기본 수집

  • toList, toSet, toMap
  • toMap 중복 키
  • toCollection

문자열

  • joining 3가지
  • vs String.join

집계

  • counting
  • summing/averaging
  • summarizing
  • minBy/maxBy

groupingBy

  • 단순
  • 다운스트림
  • 3-arg
  • 다단계

partitioningBy

  • vs groupingBy
  • 다운스트림

다운스트림

  • mapping, filtering, flatMapping
  • reducing
  • collectingAndThen

reduce

  • 3가지 형태
  • vs collect
  • identity 조건
  • 병렬 주의

9.3 Phase 10 중간 시험 50문항

람다 (10 문항)

Q1. 람다 문법? → (params) -> body
Q2. 함수형 인터페이스? → SAM
Q3. @FunctionalInterface? → 컴파일러 검증
Q4. Function 메서드? → apply
Q5. Consumer 메서드? → accept
Q6. Supplier 메서드? → get
Q7. Predicate 메서드? → test
Q8. 메서드 참조 4가지? → 정적/특정/임의/생성자
Q9. 람다의 this? → 외부
Q10. effectively final? → 실질적 final

Stream 기초 (10 문항)

Q11. Stream 정의? → 데이터 처리 파이프라인
Q12. 중간 연산 특징? → Stream 반환, 지연
Q13. 최종 연산 특징? → 결과 반환, 실행
Q14. 지연 평가? → 최종 연산 시 실행
Q15. 단락 평가? → 결과 결정 시 종료
Q16. filter? → Predicate 필터
Q17. map? → 변환
Q18. flatMap? → 평탄화
Q19. 무한 Stream? → iterate, generate
Q20. 한 번만 사용? → IllegalStateException

Stream 연산 (10 문항)

Q21. sorted? → 정렬
Q22. distinct? → 중복 제거
Q23. limit/skip? → 개수 제한/건너뛰기
Q24. forEach? → Consumer 실행
Q25. count? → 개수
Q26. anyMatch? → 첫 매칭 시 true
Q27. findFirst vs findAny? → 순서 vs 빠름
Q28. mapToInt? → boxing 회피
Q29. 병렬 Stream? → parallelStream
Q30. 병렬 빠른 경우? → 큰 데이터 + 무거운 연산

Collectors (10 문항)

Q31. Collector 구성? → supplier/accumulator/combiner/finisher
Q32. toList? → 가변 List
Q33. toMap 중복 키? → merge 함수
Q34. joining? → 문자열 결합
Q35. counting? → 개수
Q36. summingInt? → 합계
Q37. groupingBy? → Map<K, List<V>>
Q38. partitioningBy? → 2그룹 (boolean)
Q39. mapping? → 변환 후 수집
Q40. collectingAndThen? → 수집 후 변환

reduce (10 문항)

Q41. reduce 형태 1? → identity + accumulator
Q42. reduce 형태 2? → accumulator (Optional)
Q43. reduce 형태 3? → + combiner (병렬)
Q44. reduce vs collect? → immutable vs mutable
Q45. identity 조건? → combiner(identity, x) == x
Q46. 합계 identity? → 0
Q47. 곱 identity? → 1
Q48. 병렬 잘못된 identity? → 청크마다 적용
Q49. 문자열 합치기? → collect (joining) 권장
Q50. reducing 컬렉터? → groupingBy 다운스트림

9.4 채점

50 / 50 → Phase 10 핵심 마스터
45-49   → 거의 마스터
40-44   → 복습
< 40    → Unit 10.1 ~ 10.3 재학습

9.5 추가 심화 질문

Q1: Collector 의 IDENTITY_FINISH?

답:

  • finisher 가 항등 함수 (a -> a)
  • A == R (변환 X)
  • toList 등 (List 그대로)
  • collectingAndThen 은 IDENTITY_FINISH X

Q2: groupingBy 의 기본 Map?

답:

  • HashMap
  • 3-arg 로 변경 가능 (TreeMap 등)
  • groupingByConcurrent 는 ConcurrentHashMap

Q3: teeing (Java 12+)?

답:

// 두 컬렉터 결합
record MinMax(int min, int max) {}

MinMax result = numbers.stream()
    .collect(Collectors.teeing(
        Collectors.minBy(Integer::compare),
        Collectors.maxBy(Integer::compare),
        (min, max) -> new MinMax(min.get(), max.get())
    ));
// 한 번의 패스로 min, max 둘 다

Q4: reduce 의 associativity?

답:

  • 결합 법칙: (a op b) op c == a op (b op c)
  • 병렬 reduce 에서 필수
  • 덧셈, 곱셈, max, min: OK
  • 뺄셈, 나눗셈: X (병렬 시 잘못된 결과)

Q5: Collectors.toMap 의 병렬?

답:

  • 순차: 일반 HashMap
  • 병렬: combiner 로 병합
  • 중복 키 시 merge 함수 사용
  • toConcurrentMap 은 ConcurrentHashMap

🎯 핵심 요약 — 3줄 정리

1. Collectors

  • 기본 (toList/toSet/toMap), 문자열 (joining)
  • 집계 (counting/summing/averaging/summarizing)
  • groupingBy, partitioningBy + 다운스트림

2. groupingBy

  • Map<K, List>
  • 다운스트림 (counting/mapping/reducing)
  • 다단계 중첩

3. reduce

  • 3형태 (identity/Optional/combiner)
  • immutable (vs collect mutable)
  • 문자열은 joining 권장

📚 다음으로...

Unit 10.4 — Optional (3주차 완주)

이번 Unit에서 Collectors 와 reduce 를 봤다면, 마지막은 null 안전 처리.

  • Optional 의 정의
  • 생성과 활용
  • map, flatMap, filter
  • orElse, orElseGet, orElseThrow
  • 함정과 권장 패턴
  • Phase 10 완주 + 3주차 완주

Phase 10 진행 상황

🚀 Phase 10 — 함수형 프로그래밍
  ✅ Unit 10.1 람다 표현식
  ✅ Unit 10.2 Stream API
  ✅ Unit 10.3 Collectors와 reduce (★ 마스터) ← 여기
  ⏭ Unit 10.4 Optional — Phase 10 완주, 3주차 완주

3주차 누적 진행

✅ Phase 1 ~ 9 완주 (42 Unit)
🚀 Phase 10 — 함수형 프로그래밍 (3/4 진행)

마지막 Unit 10.4 완성 시 3주차 완주!
profile
Software Developer

0개의 댓글