14. Stream API
Stream API 는 컬렉션을 **선언적으로** 변형·필터·집계하는 도구입니다. `for` 반복문 + 임시 변수 코드를 한 줄짜리 파이프라인으로 바꿔주어 의도가 한눈에 드러납니다.
이 강의에서 배우는 것
- 1`stream()` 으로 스트림을 만든다
- 2`map`, `filter`, `reduce` 의 의미를 안다
- 3`collect(Collectors.toList())` 와 `Collectors.groupingBy` 를 사용한다
- 4중간 연산(intermediate) 과 최종 연산(terminal) 을 구분한다
소개
Stream API 는 컬렉션을 **선언적으로** 변형·필터·집계하는 도구입니다. `for` 반복문 + 임시 변수 코드를 한 줄짜리 파이프라인으로 바꿔주어 의도가 한눈에 드러납니다.
핵심 개념
1) 스트림 생성
List<Integer> xs = List.of(1, 2, 3);
Stream<Integer> s = xs.stream();
Stream<Integer> s2 = Stream.of(1, 2, 3);
IntStream s3 = IntStream.rangeClosed(1, 5);2) 중간 vs 최종 연산
[ source ] -> map -> filter -> map -> [ terminal ]
\---- 중간 ----/ (count/collect/forEach/...)중간 연산은 게으르게 동작하고, 최종 연산을 만나야 실제로 실행됩니다.
3) `map` / `filter` / `reduce`
int squareSum = Stream.of(1, 2, 3, 4)
.filter(n -> n % 2 == 1)
.map(n -> n * n)
.reduce(0, Integer::sum);
// 1*1 + 3*3 = 104) `collect` 와 `Collectors`
import static java.util.stream.Collectors.*;
List<String> upper = list.stream().map(String::toUpperCase).collect(toList());
Map<String, List<Person>> byCity = people.stream().collect(groupingBy(Person::city));핵심 예제
예제 1 — `StreamBasics.java` : 스트림 만들고 출력
import java.util.List;
public class StreamBasics {
public static void main(String[] args) {
List<String> langs = List.of("Java", "Kotlin", "Scala", "Groovy");
langs.stream()
.forEach(System.out::println);
long count = langs.stream().count();
System.out.println("개수=" + count);
}
}**실행 결과**
Java
Kotlin
Scala
Groovy
개수=4**메모:** `System.out::println` 은 **메서드 참조** 입니다 (18편에서 자세히).
예제 2 — `MapFilter.java` : 변환 + 거르기
import java.util.List;
import java.util.stream.Collectors;
public class MapFilter {
record Product(String name, int price) {}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Pen", 1000),
new Product("Notebook", 3000),
new Product("Bag", 25000),
new Product("Cup", 5000)
);
List<String> expensiveNames = products.stream()
.filter(p -> p.price() >= 3000)
.map(Product::name)
.collect(Collectors.toList());
System.out.println(expensiveNames);
}
}**실행 결과**
[Notebook, Bag, Cup]**메모:** `Product::name` 처럼 메서드 참조로 `getter` 호출을 짧게 표현할 수 있습니다.
예제 3 — `Reduce.java` : 누적 합·최댓값
import java.util.List;
import java.util.Optional;
public class Reduce {
public static void main(String[] args) {
List<Integer> xs = List.of(3, 1, 4, 1, 5, 9, 2, 6);
int sum = xs.stream().reduce(0, Integer::sum);
Optional<Integer> max = xs.stream().reduce(Integer::max);
System.out.println("sum=" + sum);
System.out.println("max=" + max.orElseThrow());
}
}**실행 결과**
sum=31
max=9**메모:** 시작값이 있는 reduce 는 항상 결과를 돌려주지만, 시작값이 없으면 빈 스트림 대비 `Optional` 을 받습니다.
예제 4 — `GroupingBy.java` : 그룹별 카운트/평균
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingBy {
record Student(String name, String dept, int score) {}
public static void main(String[] args) {
List<Student> ss = List.of(
new Student("Alice", "CS", 85),
new Student("Bob", "CS", 70),
new Student("Cathy", "MATH", 92),
new Student("Dan", "MATH", 65),
new Student("Eve", "PHY", 80)
);
Map<String, Double> avgByDept = ss.stream()
.collect(Collectors.groupingBy(
Student::dept,
Collectors.averagingInt(Student::score)));
avgByDept.forEach((d, a) -> System.out.printf("%s -> %.1f%n", d, a));
}
}**실행 결과 (순서 다를 수 있음)**
CS -> 77.5
MATH -> 78.5
PHY -> 80.0**메모:** `groupingBy` 는 키 추출 함수 + (선택)다운스트림 콜렉터 조합으로 강력합니다.
전체 예제 코드 (src/)
src/GroupingBy.java
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class GroupingBy {
record Student(String name, String dept, int score) {}
public static void main(String[] args) {
List<Student> ss = List.of(
new Student("Alice", "CS", 85),
new Student("Bob", "CS", 70),
new Student("Cathy", "MATH", 92),
new Student("Dan", "MATH", 65),
new Student("Eve", "PHY", 80)
);
Map<String, Double> avgByDept = ss.stream()
.collect(Collectors.groupingBy(
Student::dept,
TreeMap::new,
Collectors.averagingInt(Student::score)));
avgByDept.forEach((d, a) -> System.out.printf("%s -> %.1f%n", d, a));
}
}
src/MapFilter.java
import java.util.List;
import java.util.stream.Collectors;
public class MapFilter {
record Product(String name, int price) {}
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Pen", 1000),
new Product("Notebook", 3000),
new Product("Bag", 25000),
new Product("Cup", 5000)
);
List<String> expensiveNames = products.stream()
.filter(p -> p.price() >= 3000)
.map(Product::name)
.collect(Collectors.toList());
System.out.println(expensiveNames);
}
}
src/Reduce.java
import java.util.List;
import java.util.Optional;
public class Reduce {
public static void main(String[] args) {
List<Integer> xs = List.of(3, 1, 4, 1, 5, 9, 2, 6);
int sum = xs.stream().reduce(0, Integer::sum);
Optional<Integer> max = xs.stream().reduce(Integer::max);
System.out.println("sum=" + sum);
System.out.println("max=" + max.orElseThrow());
}
}
src/StreamBasics.java
import java.util.List;
public class StreamBasics {
public static void main(String[] args) {
List<String> langs = List.of("Java", "Kotlin", "Scala", "Groovy");
langs.stream()
.forEach(System.out::println);
long count = langs.stream().count();
System.out.println("개수=" + count);
}
}
자주 하는 실수
- 스트림 객체를 두 번 사용 (한 번 소비하면 끝)
- 무한 스트림에 `forEach` 호출 후 종료 못 함
- 부수효과(side effect) 가 있는 람다 사용 → 병렬 스트림에서 위험
- `Collectors.toList()` 와 `.toList()` (JDK 16+) 둘이 살짝 다름 (후자는 불변)
- `IntStream` 등 primitive 스트림 변환을 잊고 박싱 비용 발생
정리
- 스트림은 **선언적 데이터 처리** 파이프라인
- 중간 연산은 게으르고, 최종 연산이 트리거
- `map` / `filter` / `reduce` 만 익히면 80% 활용 가능
- 그룹/집계가 필요할 때는 `Collectors` 가 강력
과제
# 과제 - 14. Stream API
## 문제 1 — 학생 점수 분석
- 파일명: `Homework01.java`
- 핵심 개념: `map`, `filter`, `reduce`, `Collectors.groupingBy`
요구사항
- `record Student(String name, String dept, int score)`
- 다음 데이터로:
- Alice CS 85, Bob CS 70, Cathy MATH 92, Dan MATH 65, Eve PHY 80
- 전체 평균, 최고점 학생 이름, 학과별 평균을 출력하세요.
예상 출력 (학과 순서는 다를 수 있음)
전체 평균: 78.4
최고점: Cathy (92)
학과별 평균
CS -> 77.5
MATH -> 78.5
PHY -> 80.0## 문제 2 — 짝수 제곱의 합
- 파일명: `Homework02.java`
- 핵심 개념: `IntStream`, `filter`, `map`, `sum`
요구사항
- 1 ~ 10 의 정수 중 짝수만 골라 제곱하고, 그 합을 출력합니다.
예상 출력
짝수 제곱 합 = 220## 정답 확인 직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.
정답 코드 (homework/answer/)
answer/Homework01.java
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/** 학생 점수 분석. */
public class Homework01 {
record Student(String name, String dept, int score) {}
public static void main(String[] args) {
List<Student> ss = List.of(
new Student("Alice", "CS", 85),
new Student("Bob", "CS", 70),
new Student("Cathy", "MATH", 92),
new Student("Dan", "MATH", 65),
new Student("Eve", "PHY", 80)
);
double avg = ss.stream().mapToInt(Student::score).average().orElse(0);
Student best = ss.stream().max(Comparator.comparingInt(Student::score)).orElseThrow();
Map<String, Double> byDept = ss.stream()
.collect(Collectors.groupingBy(
Student::dept,
TreeMap::new,
Collectors.averagingInt(Student::score)));
System.out.printf("전체 평균: %.1f%n", avg);
System.out.println("최고점: " + best.name() + " (" + best.score() + ")");
System.out.println("학과별 평균");
byDept.forEach((d, a) -> System.out.printf("%s -> %.1f%n", d, a));
}
}
answer/Homework02.java
import java.util.stream.IntStream;
/** 1~10 의 짝수 제곱 합. */
public class Homework02 {
public static void main(String[] args) {
int sum = IntStream.rangeClosed(1, 10)
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.sum();
System.out.println("짝수 제곱 합 = " + sum);
}
}
직접 해 보기
cd 03_컬렉션_제네릭/14_Stream_API/src
javac MapFilter.java
java MapFilter다음 단원
[15_예외처리](../../04_예외_입출력/15_예외처리/) — `try`/`catch`, checked vs unchecked, 사용자 정의 예외를 배웁니다.