전체 목록
JavaMedium#059

Java 8의 Stream API를 설명하고 주요 연산들을 예시와 함께 설명해주세요.

#Java#Stream#함수형#Java8

답변 포인트

중간 연산과 최종 연산, 지연 평가(lazy evaluation)를 생각해보세요.

정답 및 해설

빠른 요약

Stream API는 컬렉션을 선언적으로 처리하는 함수형 API입니다. 중간 연산 (lazy, 반환값 Stream): - filter(predicate): 조건에 맞는 요소만 - map(function): 요소 변환 - flatMap: 중첩 스트림 평탄화 - sorted, distinct, limit, skip 최종 연산 (eager, 스트림 소비): - collect(Collectors.toList()) - forEach, count, anyMatch, allMatch - reduce: 누적 계산 - findFirst, min, max 지연 평가: 최종 연산이 호출될 때 중간 연산이 실행됩니다.

Stream API는 Java 8에서 도입된 컬렉션을 선언적(declarative)으로 처리하는 함수형 API입니다. 반복문을 직접 작성하는 대신, 무엇을 할지를 표현하여 코드의 가독성과 유지보수성을 높입니다.

Stream의 특징

  • 선언적: 어떻게(how)가 아닌 무엇을(what) 처리할지 표현
  • 지연 평가(Lazy Evaluation): 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음
  • 일회성: 한 번 소비(최종 연산)된 스트림은 재사용 불가
  • 원본 불변: 원본 컬렉션을 변경하지 않음
  • 병렬 처리: parallelStream()으로 간단히 병렬화 가능
Java
// 명령형 방식 (for-loop)
List<String> result = new ArrayList<>();
for (String name : names) {
    if (name.startsWith("A")) {
        result.add(name.toUpperCase());
    }
}

// Stream 방식 (선언적)
List<String> result = names.stream()
    .filter(name -> name.startsWith("A"))
    .map(String::toUpperCase)
    .collect(Collectors.toList());

스트림 생성

Java
// 컬렉션으로부터
List<String> list = List.of("a", "b", "c");
Stream<String> streamFromList = list.stream();

// 배열로부터
String[] array = {"a", "b", "c"};
Stream<String> streamFromArray = Arrays.stream(array);

// 직접 생성
Stream<String> streamOf = Stream.of("a", "b", "c");

// 범위 (정수)
IntStream range = IntStream.range(1, 6);       // 1, 2, 3, 4, 5
IntStream rangeClosed = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5

// 무한 스트림
Stream<Integer> iterate = Stream.iterate(0, n -> n + 2).limit(5); // 0, 2, 4, 6, 8
Stream<Double> generate = Stream.generate(Math::random).limit(3);

중간 연산 (Intermediate Operations)

중간 연산은 항상 스트림을 반환하며, 지연(lazy) 평가됩니다. 여러 개를 체이닝할 수 있습니다.

filter — 조건에 맞는 요소만 선택

Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> evens = numbers.stream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList());
// [2, 4, 6, 8, 10]

map — 요소를 다른 타입/값으로 변환

Java
List<String> names = List.of("alice", "bob", "charlie");

List<String> upperNames = names.stream()
    .map(String::toUpperCase)
    .collect(Collectors.toList());
// [ALICE, BOB, CHARLIE]

List<Integer> nameLengths = names.stream()
    .map(String::length)
    .collect(Collectors.toList());
// [5, 3, 7]

flatMap — 중첩 스트림을 평탄화

Java
List<List<Integer>> nested = List.of(
    List.of(1, 2, 3),
    List.of(4, 5),
    List.of(6, 7, 8, 9)
);

List<Integer> flat = nested.stream()
    .flatMap(Collection::stream) // 각 리스트를 스트림으로 펼침
    .collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

// 단어 분리 예시
List<String> sentences = List.of("Hello World", "Java Stream");
List<String> words = sentences.stream()
    .flatMap(s -> Arrays.stream(s.split(" ")))
    .collect(Collectors.toList());
// [Hello, World, Java, Stream]

sorted — 정렬

Java
List<String> names = List.of("banana", "apple", "cherry");

List<String> sorted = names.stream()
    .sorted() // 자연 순서
    .collect(Collectors.toList());
// [apple, banana, cherry]

List<String> sortedByLength = names.stream()
    .sorted(Comparator.comparingInt(String::length))
    .collect(Collectors.toList());
// [apple, banana, cherry] → 길이 기준

distinct, limit, skip

Java
List<Integer> nums = List.of(1, 2, 2, 3, 3, 3, 4);

List<Integer> unique = nums.stream()
    .distinct()
    .collect(Collectors.toList());
// [1, 2, 3, 4]

List<Integer> firstThree = nums.stream()
    .limit(3)
    .collect(Collectors.toList());
// [1, 2, 2]

List<Integer> afterTwo = nums.stream()
    .skip(2)
    .collect(Collectors.toList());
// [2, 3, 3, 3, 4]

최종 연산 (Terminal Operations)

최종 연산은 스트림을 **소비(consume)**하고 결과를 반환합니다. 호출 후 스트림 재사용 불가합니다.

collect — 결과를 컬렉션으로 수집

Java
List<String> names = List.of("Alice", "Bob", "Charlie", "Alice");

// List로 수집
List<String> list = names.stream().collect(Collectors.toList());

// Set으로 수집 (중복 제거)
Set<String> set = names.stream().collect(Collectors.toSet());

// Map으로 수집
Map<String, Integer> nameToLength = names.stream()
    .distinct()
    .collect(Collectors.toMap(
        name -> name,           // 키 매핑
        String::length          // 값 매핑
    ));
// {Alice=5, Bob=3, Charlie=7}

// 그룹화
Map<Integer, List<String>> byLength = names.stream()
    .collect(Collectors.groupingBy(String::length));
// {3=[Bob], 5=[Alice, Alice], 7=[Charlie]}

// 문자열 조인
String joined = names.stream()
    .collect(Collectors.joining(", ", "[", "]"));
// [Alice, Bob, Charlie, Alice]

forEach, count, anyMatch, allMatch, noneMatch

Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// forEach
numbers.stream().forEach(System.out::println);

// count
long count = numbers.stream().filter(n -> n > 3).count(); // 2

// anyMatch — 하나라도 조건 만족
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true

// allMatch — 모두 조건 만족
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true

// noneMatch — 아무것도 조건 만족 안 함
boolean noneNegative = numbers.stream().noneMatch(n -> n < 0); // true

reduce — 요소를 하나의 값으로 축소

Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// 합산
int sum = numbers.stream().reduce(0, Integer::sum); // 15

// 최대값
Optional<Integer> max = numbers.stream().reduce(Integer::max); // Optional[5]

// 문자열 결합
List<String> words = List.of("Java", "is", "cool");
String sentence = words.stream().reduce("", (a, b) -> a + " " + b).strip();
// "Java is cool"

findFirst, min, max

Java
List<String> names = List.of("Charlie", "Alice", "Bob");

Optional<String> first = names.stream()
    .filter(n -> n.startsWith("A"))
    .findFirst(); // Optional[Alice]

Optional<String> shortest = names.stream()
    .min(Comparator.comparingInt(String::length)); // Optional[Bob]

Optional<String> longest = names.stream()
    .max(Comparator.comparingInt(String::length)); // Optional[Charlie]

지연 평가 (Lazy Evaluation) 예시

Java
List<Integer> numbers = List.of(1, 2, 3, 4, 5);

// 최종 연산이 없으면 아무것도 실행되지 않음
Stream<Integer> stream = numbers.stream()
    .filter(n -> {
        System.out.println("filter: " + n);
        return n % 2 == 0;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * 10;
    });
// 여기까지 아무 출력 없음!

List<Integer> result = stream.collect(Collectors.toList());
// 최종 연산 호출 시 비로소 실행됨
// filter: 1, filter: 2, map: 2, filter: 3, filter: 4, map: 4, filter: 5
// result: [20, 40]

실전 활용 예시

Java
// 직원 목록에서 부서별 평균 연봉 계산
Map<String, Double> avgSalaryByDept = employees.stream()
    .collect(Collectors.groupingBy(
        Employee::getDepartment,
        Collectors.averagingDouble(Employee::getSalary)
    ));

// 상위 3명의 고연봉자 이름 조회
List<String> top3 = employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .limit(3)
    .map(Employee::getName)
    .collect(Collectors.toList());

// 전체 급여 합산
double totalSalary = employees.stream()
    .mapToDouble(Employee::getSalary)
    .sum();

중간 연산 vs 최종 연산 정리

구분연산반환 타입실행 시점
중간 연산filter, map, flatMap, sorted, distinct, limit, skipStreamlazy (지연)
최종 연산collect, forEach, count, reduce, findFirst, anyMatch, allMatch, min, max결과값eager (즉시)

관련 질문

같은 카테고리/태그 기준