F-LAB JAVA · 3주차 · Phase 10 · 함수형 프로그래밍
★ 마스터 Unit — Collectors 의 모든 것
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
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 의 최종 처리.
1. Collector 인터페이스의 구조
2. 기본 수집 (toList, toSet, toMap)
3. 문자열 결합 (joining)
4. 집계 (counting, summing, averaging)
5. groupingBy 의 정밀
6. partitioningBy
7. 다운스트림 컬렉터
8. reduce 의 3가지 형태
9. 면접 + 자기 점검 + Phase 10 중간 시험
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. 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)
collect 의 동작:
순차:
1. supplier() — 컨테이너 생성
2. 각 요소마다 accumulator() 호출
3. finisher() — 최종 변환
(combiner 사용 X)
병렬:
1. 각 청크마다 supplier() — 여러 컨테이너
2. 각 청크에서 accumulator()
3. combiner() — 컨테이너들 병합
4. finisher() — 최종 변환
// 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 없음)
enum Characteristics {
CONCURRENT, // 동시 accumulator 호출 가능
UNORDERED, // 순서 무관
IDENTITY_FINISH // finisher 가 항등 (생략 가능)
}
// 의미:
// - CONCURRENT: 단일 컨테이너 동시 접근
// - UNORDERED: 결과 순서 무관 (Set 등)
// - IDENTITY_FINISH: A == R (변환 X)
// 사용자 정의 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"
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());
}
}
Collector 인터페이스의 4가지 구성 요소는?
답:
1. supplier: 컨테이너 생성
2. accumulator: 요소 누적
3. combiner: 병렬 병합
4. finisher: 최종 변환
+ characteristics: CONCURRENT, UNORDERED, IDENTITY_FINISH
동작:
// 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());
// 중복 제거 + 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));
// 키 함수 + 값 함수
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()
));
// 중복 키 발생 시 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)
// 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
));
// 특정 컬렉션 지정
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))));
| Collector | 결과 | 가변? |
|---|---|---|
| toList() | List (ArrayList) | 가변 |
| toUnmodifiableList() | List | 불변 |
| toSet() | Set (HashSet) | 가변 |
| toUnmodifiableSet() | Set | 불변 |
| toMap(k, v) | Map (HashMap) | 가변 |
| toCollection(supplier) | 지정 컬렉션 | 가변 |
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))));
}
}
기본 수집 컬렉터는?
답:
1. toList():
toSet():
toMap(k, v):
toCollection(supplier):
불변 버전:
// 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]"
// 객체 → 문자열 → 결합
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"]
// 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());
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')"
}
}
joining 의 사용은?
답:
1. 3가지 형태:
활용:
vs String.join:
// 개수 세기
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}
// 합계
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())
));
// 평균
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())
));
// 통계 한 번에 (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()));
// 최소/최대 (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))
));
// 컬렉터로서의 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
)
));
| Collector | 결과 | 의미 |
|---|---|---|
| counting() | Long | 개수 |
| summingInt/Long/Double | Integer/Long/Double | 합계 |
| averagingInt/Long/Double | Double | 평균 |
| summarizingInt/Long/Double | XxxSummaryStatistics | 모든 통계 |
| minBy/maxBy | Optional | 최소/최대 |
| reducing | T 또는 Optional | 축약 |
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())
));
}
}
집계 컬렉터는?
답:
1. counting(): 개수 (Long)
2. summingInt/Long/Double: 합계
3. averagingInt/Long/Double: 평균 (Double)
4. summarizingInt/Long/Double: 모든 통계
5. minBy/maxBy: Optional
6. reducing: 축약
활용:
// 분류 함수로 그룹핑
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]
// }
// 분류 + 각 그룹에 추가 컬렉터
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())
));
// 분류 + 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()
));
// 그룹 안에 그룹 (중첩)
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()
)
));
// 병렬 + 동시성 Map
ConcurrentMap<String, List<Shipment>> concurrent = shipments.parallelStream()
.collect(Collectors.groupingByConcurrent(Shipment::getStatus));
// 차이:
// - groupingBy: 순차 또는 병렬 (병합)
// - groupingByConcurrent: ConcurrentHashMap, 진짜 동시 누적
// - 큰 병렬 데이터에서 효율
// 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())
));
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()
)
));
}
}
groupingBy 의 정밀은?
답:
1. 단순:
groupingBy(classifier)다운스트림:
groupingBy(classifier, downstream)3-arg:
groupingBy(classifier, mapFactory, downstream)다단계:
groupingByConcurrent:
// 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);
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: [...]}
// 다운스트림 컬렉터
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())
));
// 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)
));
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) {}
}
partitioningBy 와 groupingBy 의 차이는?
답:
1. partitioningBy:
groupingBy:
공통:
활용:
다운스트림 컬렉터:
groupingBy / partitioningBy 의 각 그룹에
추가로 적용하는 컬렉터.
종류:
- counting
- summing/averaging/summarizing
- mapping
- filtering (Java 9+)
- flatMapping (Java 9+)
- reducing
- collectingAndThen
- toList, toSet
// 각 요소를 변환 후 수집
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())
));
// 그룹 내 필터링
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())
));
// 모든 상태 키 유지 (빈 리스트라도)
// 그룹 내 평탄화
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()
)
));
// 각 상태의 모든 아이템 평탄화
// 그룹 내 축약
Collectors.reducing(identity, mapper, op);
// 활용 — 그룹별 총 중량
Map<String, BigDecimal> weightByStatus = shipments.stream()
.collect(Collectors.groupingBy(
Shipment::getStatus,
Collectors.reducing(
BigDecimal.ZERO,
Shipment::getWeight,
BigDecimal::add
)
));
// 수집 후 변환
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
)
));
// 복잡한 조합
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(", ")
)
));
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(", ", "[", "]")
)
));
}
}
다운스트림 컬렉터는?
답:
1. 종류:
mapping vs filter:
collectingAndThen:
조합:
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 시 반환값
// - 초기 누적값
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);
<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
);
| 형태 | 시그니처 | 반환 | 활용 |
|---|---|---|---|
| 1 | reduce(identity, acc) | T | 같은 타입 축약 |
| 2 | reduce(acc) | Optional | identity 없음 |
| 3 | reduce(identity, acc, combiner) | U | 다른 타입 + 병렬 |
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))
// 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 + 모든 합 (잘못된 결과)
// 특히 병렬에서 문제
// 병렬에서 잘못된 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 에서 둘이 호환되어야
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 보다 효율적
}
}
reduce 의 3가지 형태는?
답:
1. 형태 1:
reduce(identity, accumulator)형태 2:
reduce(accumulator)형태 3:
reduce(identity, accumulator, combiner)reduce vs collect:
identity 조건:
| 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? | 수집 후 변환 |
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
Q11. Stream 정의? → 데이터 처리 파이프라인
Q12. 중간 연산 특징? → Stream 반환, 지연
Q13. 최종 연산 특징? → 결과 반환, 실행
Q14. 지연 평가? → 최종 연산 시 실행
Q15. 단락 평가? → 결과 결정 시 종료
Q16. filter? → Predicate 필터
Q17. map? → 변환
Q18. flatMap? → 평탄화
Q19. 무한 Stream? → iterate, generate
Q20. 한 번만 사용? → IllegalStateException
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. 병렬 빠른 경우? → 큰 데이터 + 무거운 연산
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? → 수집 후 변환
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 다운스트림
50 / 50 → Phase 10 핵심 마스터
45-49 → 거의 마스터
40-44 → 복습
< 40 → Unit 10.1 ~ 10.3 재학습
답:
답:
답:
// 두 컬렉터 결합
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 둘 다
답:
답:
1. Collectors
2. groupingBy
3. reduce
이번 Unit에서 Collectors 와 reduce 를 봤다면, 마지막은 null 안전 처리.
🚀 Phase 10 — 함수형 프로그래밍
✅ Unit 10.1 람다 표현식
✅ Unit 10.2 Stream API
✅ Unit 10.3 Collectors와 reduce (★ 마스터) ← 여기
⏭ Unit 10.4 Optional — Phase 10 완주, 3주차 완주
✅ Phase 1 ~ 9 완주 (42 Unit)
🚀 Phase 10 — 함수형 프로그래밍 (3/4 진행)
마지막 Unit 10.4 완성 시 3주차 완주!