컬렉션을 선언적으로 처리하는 파이프라인. "어떻게"가 아니라 "무엇을"을 적는다.
세 부분
list.stream() // 소스
.filter(m -> m.age() > 20) // 중간 연산 — 실행되지 않는다
.map(Member::name) // 중간 연산
.toList(); // 최종 연산 — 이때 비로소 흐른다
지연 평가가 만드는 것
list.stream()
.filter(x -> { System.out.println("filter " + x); return x > 2; })
.map(x -> { System.out.println("map " + x); return x * 10; })
.findFirst();
출력 filter 1 filter 2 filter 3
- map 3 — ← 3 을 찾자마자 멈춘다. 나머지는 아예 안 본다
각 원소가 파이프라인 '전체' 를 통과한 뒤 다음 원소로 간다 (원소 단위 수직 처리)
- 무한 스트림도 다룰 수 있고, 불필요한 연산을 건너뛴다
한 번 쓰면 끝이다
Stream<String> s = list.stream();
s.forEach(...);
s.forEach(...); // IllegalStateException: stream has already been operated upon or closed
부작용을 넣지 않는다
// ❌ 외부 리스트에 담는다 — 병렬에서 깨지고 의도도 흐려진다
List<String> result = new ArrayList<>();
list.stream().filter(...).forEach(result::add);
// ✅ 수집은 collect 로
List<String> result = list.stream().filter(...).map(...).toList();
그룹핑
Map<String, List<Member>> byCity =
list.stream().collect(Collectors.groupingBy(Member::city));
Map<String, Long> countByCity =
list.stream().collect(Collectors.groupingBy(Member::city, Collectors.counting()));
병렬 스트림은 신중하게
list.parallelStream().map(this::heavy).toList();
-
공용 ForkJoinPool 을 쓴다 (코어 수 - 1). 다른 병렬 작업과 공유된다
-
여기에 블로킹 I/O 를 넣으면 애플리케이션 전체의 병렬 처리가 막힌다
-
원소가 적거나 연산이 가벼우면 분할·병합 비용이 이득보다 크다
-
순서가 있는 소스(LinkedList)나 상태를 가진 람다에서는 오히려 느리거나 틀린다
-
기준: CPU 바운드 + 원소가 충분히 많다 + 연산이 독립적이다 → 그때만
박싱을 피한다
Stream<Integer> s; // 원소마다 Integer 객체
IntStream.range(0, n).sum(); // 박싱 없음. 대량 수치 연산은 반드시 이쪽
언제 안 쓰나
- 단순한 for 문이 더 읽기 쉬울 때 (원소 하나 찾기 · 인덱스가 필요할 때)
- 검사 예외를 던져야 할 때
- 중간에 break·continue 로 복잡한 흐름 제어가 필요할 때
면접 함정
- ❌ "스트림이 for 문보다 빠르다" → 대개 비슷하거나 조금 느리다. 가독성과 병렬화 가능성이 이득이다.
- ❌ "parallelStream을 쓰면 빨라진다" → 조건이 맞아야 한다. 잘못 쓰면 훨씬 느리다.