자바 언어·플랫폼 용어 사전
함수형지연 평가 · 최종 연산 · parallelStream

Stream

데이터 처리 파이프라인. 최종 연산이 불릴 때까지 아무것도 실행되지 않는다.

컬렉션을 선언적으로 처리하는 파이프라인. "어떻게"가 아니라 "무엇을"을 적는다.

세 부분

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을 쓰면 빨라진다" → 조건이 맞아야 한다. 잘못 쓰면 훨씬 느리다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 함수형 프로그래밍·Stream