Java Stream API | filter, map, reduce 완벽 정리
이 글의 핵심
Java Stream API에 대한 실전 가이드입니다. filter, map, reduce 완벽 정리 등을 예제와 함께 상세히 설명합니다.
들어가며
Stream API는 Java 8에서 도입된 함수형 스타일로 컬렉션을 처리하는 도구입니다.
비유(컨베이어 벨트): 리스트·배열의 요소가 컨베이어 벨트 위를 지나가며 filter로 걸러지고 map으로 바뀌고, 마지막에 collect 등으로 최종 연산에서 꺼내 담습니다. 한 번 소비한 스트림은 다시 쓸 수 없다는 점도, 벨트 끝까지 간 물건을 같은 줄로 두 번 보낼 수 없다고 이해하시면 됩니다.
1. Stream 생성
다양한 생성 방법
import java.util.*;
import java.util.stream.*;
// 컬렉션에서
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> stream1 = numbers.stream();
// 배열에서
String[] arr = {"a", "b", "c"};
Stream<String> stream2 = Arrays.stream(arr);
// 직접 생성
Stream<String> stream3 = Stream.of("a", "b", "c");
// 빈 스트림
Stream<String> empty = Stream.empty();
// 무한 스트림
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
Stream<Double> random = Stream.generate(Math::random);
// 범위
IntStream range = IntStream.range(1, 10); // 1~9
IntStream rangeClosed = IntStream.rangeClosed(1, 10); // 1~10
2. 중간 연산 (Intermediate Operations)
filter - 필터링
조건에 맞는 요소만 선택하는 중간 연산입니다:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// filter: 조건을 만족하는 요소만 통과
// n -> n % 2 == 0: 람다 표현식 (짝수 판별)
// collect: 최종 연산, Stream을 List로 변환
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.collect(Collectors.toList());
System.out.println(evens); // [2, 4, 6, 8, 10]
// 여러 filter 체이닝 가능
// 각 filter는 순차적으로 적용됨
List<Integer> filtered = numbers.stream()
.filter(n -> n > 3) // 3보다 큰 수 [4, 5, 6, 7, 8, 9, 10]
.filter(n -> n < 8) // 8보다 작은 수 [4, 5, 6, 7]
.collect(Collectors.toList());
System.out.println(filtered); // [4, 5, 6, 7]
// 복잡한 조건
List<Integer> complex = numbers.stream()
.filter(n -> n % 2 == 0 && n > 5) // 5보다 큰 짝수
.collect(Collectors.toList());
System.out.println(complex); // [6, 8, 10]
filter의 동작 원리:
- Stream의 각 요소에 대해 조건 함수(Predicate) 실행
- 조건이 true인 요소만 다음 단계로 전달
- 조건이 false인 요소는 제거됨
- 원본 컬렉션은 변경되지 않음 (불변성)
map - 변환
// 2배로
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
// 문자열로
List<String> strings = numbers.stream()
.map(n -> "숫자: " + n)
.collect(Collectors.toList());
// 객체 변환
class User {
String name;
int age;
User(String name, int age) {
this.name = name;
this.age = age;
}
}
List<User> users = Arrays.asList(
new User("홍길동", 25),
new User("김철수", 30)
);
List<String> names = users.stream()
.map(u -> u.name)
.collect(Collectors.toList());
flatMap - 평탄화
List<List<Integer>> nested = Arrays.asList(
Arrays.asList(1, 2),
Arrays.asList(3, 4),
Arrays.asList(5, 6)
);
List<Integer> flattened = nested.stream()
.flatMap(list -> list.stream())
.collect(Collectors.toList());
System.out.println(flattened); // [1, 2, 3, 4, 5, 6]
distinct, sorted, limit, skip
List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);
// 중복 제거
List<Integer> unique = numbers.stream()
.distinct()
.collect(Collectors.toList());
// 정렬
List<Integer> sorted = numbers.stream()
.sorted()
.collect(Collectors.toList());
// 역순 정렬
List<Integer> reversed = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// 상위 3개
List<Integer> top3 = numbers.stream()
.sorted(Comparator.reverseOrder())
.limit(3)
.collect(Collectors.toList());
// 처음 2개 건너뛰기
List<Integer> skipped = numbers.stream()
.skip(2)
.collect(Collectors.toList());
3. 최종 연산 (Terminal Operations)
reduce - 집계
Stream의 요소들을 하나의 값으로 줄이는(reduce) 연산입니다:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// reduce: 누적 연산
// 첫 번째 인자(0): 초기값 (identity)
// 두 번째 인자: 누적 함수 (accumulator)
// a: 누적값, b: 현재 요소
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
// 동작 과정:
// 1. a=0 (초기값), b=1 → 0+1=1
// 2. a=1 (이전 결과), b=2 → 1+2=3
// 3. a=3, b=3 → 3+3=6
// 4. a=6, b=4 → 6+4=10
// 5. a=10, b=5 → 10+5=15
System.out.println(sum); // 15
// 곱셈: 초기값을 1로 설정
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
// 1 * 1 * 2 * 3 * 4 * 5 = 120
System.out.println(product); // 120
// 최댓값: 초기값 없이 사용 (Optional 반환)
Optional<Integer> max = numbers.stream()
.reduce((a, b) -> a > b ? a : b);
// 동작: 두 값을 비교해서 큰 값을 누적
// 1과 2 비교 → 2
// 2와 3 비교 → 3
// 3과 4 비교 → 4
// 4와 5 비교 → 5
max.ifPresent(System.out::println); // 5
// 실전 예시: 문자열 연결
List<String> words = Arrays.asList("Hello", "World", "Java");
String sentence = words.stream()
.reduce("", (a, b) -> a + " " + b);
System.out.println(sentence.trim()); // Hello World Java
reduce의 세 가지 형태:
reduce(identity, accumulator): 초기값 있음, 결과 타입 확정reduce(accumulator): 초기값 없음, Optional 반환reduce(identity, accumulator, combiner): 병렬 스트림용
언제 사용하나:
- 합계, 곱셈, 최대/최소값 계산
- 문자열 연결
- 커스텀 집계 연산
collect - 수집
// List로
List<Integer> list = numbers.stream()
.collect(Collectors.toList());
// Set으로
Set<Integer> set = numbers.stream()
.collect(Collectors.toSet());
// Map으로
Map<String, Integer> map = users.stream()
.collect(Collectors.toMap(
u -> u.name,
u -> u.age
));
// 그룹화
Map<Integer, List<User>> byAge = users.stream()
.collect(Collectors.groupingBy(u -> u.age));
// 문자열 결합
String joined = numbers.stream()
.map(String::valueOf)
.collect(Collectors.joining(", "));
System.out.println(joined); // "1, 2, 3, 4, 5"
forEach, count, anyMatch
// forEach
numbers.stream()
.forEach(System.out::println);
// count
long count = numbers.stream()
.filter(n -> n > 3)
.count();
// anyMatch, allMatch, noneMatch
boolean hasEven = numbers.stream()
.anyMatch(n -> n % 2 == 0);
boolean allPositive = numbers.stream()
.allMatch(n -> n > 0);
boolean noNegative = numbers.stream()
.noneMatch(n -> n < 0);
4. 병렬 스트림
List<Integer> numbers = IntStream.rangeClosed(1, 1000000)
.boxed()
.collect(Collectors.toList());
// 순차 스트림
long start = System.currentTimeMillis();
int sum1 = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
long time1 = System.currentTimeMillis() - start;
// 병렬 스트림
start = System.currentTimeMillis();
int sum2 = numbers.parallelStream()
.mapToInt(Integer::intValue)
.sum();
long time2 = System.currentTimeMillis() - start;
System.out.println("순차: " + time1 + "ms");
System.out.println("병렬: " + time2 + "ms");
5. 실전 예제
예제: 사용자 데이터 처리
class User {
String name;
int age;
boolean active;
User(String name, int age, boolean active) {
this.name = name;
this.age = age;
this.active = active;
}
}
List<User> users = Arrays.asList(
new User("홍길동", 25, true),
new User("김철수", 17, false),
new User("이영희", 30, true),
new User("박민수", 22, true)
);
// 활성 성인 사용자 이름
List<String> activeAdults = users.stream()
.filter(u -> u.active)
.filter(u -> u.age >= 18)
.map(u -> u.name)
.collect(Collectors.toList());
System.out.println(activeAdults); // [홍길동, 이영희, 박민수]
// 평균 나이
double avgAge = users.stream()
.filter(u -> u.active)
.mapToInt(u -> u.age)
.average()
.orElse(0);
System.out.println("평균 나이: " + avgAge);
정리
핵심 요약
- Stream 생성: stream(), of(), range()
- 중간 연산: filter, map, flatMap, distinct, sorted
- 최종 연산: reduce, collect, forEach, count
- 병렬 처리: parallelStream()
- Collectors: toList, toSet, toMap, groupingBy
다음 단계
- Java 람다
- Java 예외 처리
- Java I/O
관련 글
- Java 람다와 함수형 인터페이스 | Lambda Expression
- C++ 스트림 I/O |
- Java 시작하기 | JDK 설치부터 Hello World까지
- Java 변수와 타입 | 기본 타입, 참조 타입, 형변환
- Java 클래스와 객체 | OOP, 상속, 인터페이스