전체 목록
JavaMedium#59

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

#Java#Stream#함수형#Java8
힌트

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

정답 및 해설

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

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

Stream의 특징

  • 선언적: 어떻게(how)가 아닌 무엇을(what) 처리할지 표현
  • 지연 평가(Lazy Evaluation): 최종 연산이 호출될 때까지 중간 연산이 실행되지 않음
  • 일회성: 한 번 소비(최종 연산)된 스트림은 재사용 불가
  • 원본 불변: 원본 컬렉션을 변경하지 않음
  • 병렬 처리: parallelStream()으로 간단히 병렬화 가능
// 명령형 방식 (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());

스트림 생성

// 컬렉션으로부터
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 — 조건에 맞는 요소만 선택

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 — 요소를 다른 타입/값으로 변환

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 — 중첩 스트림을 평탄화

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 — 정렬

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

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 — 결과를 컬렉션으로 수집

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

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 — 요소를 하나의 값으로 축소

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

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) 예시

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]

실전 활용 예시

// 직원 목록에서 부서별 평균 연봉 계산
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 (즉시)