Java Stream API | filter, map, reduce 완벽 정리

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의 동작 원리:

  1. Stream의 각 요소에 대해 조건 함수(Predicate) 실행
  2. 조건이 true인 요소만 다음 단계로 전달
  3. 조건이 false인 요소는 제거됨
  4. 원본 컬렉션은 변경되지 않음 (불변성)

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의 세 가지 형태:

  1. reduce(identity, accumulator): 초기값 있음, 결과 타입 확정
  2. reduce(accumulator): 초기값 없음, Optional 반환
  3. 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);

정리

핵심 요약

  1. Stream 생성: stream(), of(), range()
  2. 중간 연산: filter, map, flatMap, distinct, sorted
  3. 최종 연산: reduce, collect, forEach, count
  4. 병렬 처리: parallelStream()
  5. Collectors: toList, toSet, toMap, groupingBy

다음 단계

  • Java 람다
  • Java 예외 처리
  • Java I/O

관련 글

  • Java 람다와 함수형 인터페이스 | Lambda Expression
  • C++ 스트림 I/O |
  • Java 시작하기 | JDK 설치부터 Hello World까지
  • Java 변수와 타입 | 기본 타입, 참조 타입, 형변환
  • Java 클래스와 객체 | OOP, 상속, 인터페이스