Java Stream 최종 연산

김영한님의 김영한의 실전 자바 – 고급 3편, 람다, 스트림, 함수형 프로그래밍 내용 중 일부로 실무에서 자주 사용될 법한 Stream의 최종 연산 종류들이다.

Stream에서의 최종 연산

최종 연산(Terminal Operation)은 스트림 파이프라인의 끝에 호출되어 실제 연산을 수행하고 결과는 만든다. 최종 연산이 실행된 후에 스트림은 소모되어 더 이상 사용할 수 없다

1-1. Collect(Collectors)

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

List<Integer> evenNumber = numbers.stream()
                .filter(n -> n % 2 == 0)
                .collect(Collectors.toList());

System.out.println(evenNumber); // [2, 2, 4, 6, 8, 10]
  • Collector를 사용하여 결과 수집을 하며 다양한 형태로 변환 가능하다
    • 복잡한 수집이 필요할 때 사용

1-2. toList()

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

List<Integer> evenNumber = numbers.stream()
                .filter(n -> n % 2 == 0)
                .toList();

System.out.println(evenNumber); // [2, 2, 4, 6, 8, 10]
  • Collectors.toList() 대신 stream.toList()를 써서 간단하게 List로 변환 (Java 16 +)

2. toArray()

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

Integer[] arrEvenNumbers = numbers.stream()
                .filter(n -> n % 2 == 0)
                .toArray(Integer[]::new);

System.out.println(Arrays.toString(arrEvenNumbers)); // [2, 2, 4, 6, 8, 10]
  • 스트림을 배열로 반환

3. forEach

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

numbers.stream()
        .limit(5) // 5개만
        .forEach(n -> System.out.print(n + " ")); // 1 2 2 3 4 
  • 각 요소에 대한 동작을 순차적으로 수행하며 반환값은 없다

4. count

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

long count = numbers.stream()
                .filter(n -> n > 5)
                .count();

System.out.println(count); // 5
  • 스트림의 요소 개수를 반환한다

Optional

  • Optional은 “결과값이 없을 수도 있음”을 명시적으로 알려주는 컨테이너
  • null을 직접 반환하는 대신 Optional로 값을 감싸면, 이 값을 사용하는 개발자가 값이 없는 경우를 반드시 인지하고 처리하도록 강제하여 NPE을 원천 방지
  • Optional을 올바르게 사용하면 “값이 없을 때 어떻게 하지?”에 대한 처리를 코드에 명확히 녹여내어 방어적인 if (value != null) 구문을 제거하고 가독성 높은 코드를 작성할 수 있다

5-1. reduce (초기값 없음)

  • 요소들을 하나의 값으로 누적 (합계, 곱, 최소값, 최대값 등)
List<Integer> numbers = List.of(1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 10);

Optional<Integer> sum = numbers.stream()
                .reduce((a, b) -> a + b);

System.out.println(sum.get()); // 62
  • 첫 번째 요소와 두 번째 요소로 연산을 수행하고, 그 결과와 세 번째 요소를 가지고 또 다시 연산하는 과정을 마지막 요소까지 반복
  • Stream이 비어있어 연산할 요소가 하나도 없으면 결과값을 만들 수 없어 Optional 반환

5.2 reduce (초기값 있음)

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

int sum = numbers.stream()
                .reduce(100, Integer::sum);

System.out.println(sum.get()); // 162
  • 초기값을 지정하면, 모든 누적 연산이 바로 그 초기값에서 시작
  • 스트림이 비어 있더라도 항상 초기값이 그대로 반환되므로 결과는 Optional이 아닌 확정된 타입

6. min

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

// 메서드 참조
Optional<Integer> min = numbers.stream()
                .min(Integer::compareTo);

// 람다
Optional<Integer> min = numbers.stream()
                .min((integer, anotherInteger) -> integer.compareTo(anotherInteger));

// 알고리즘 구현
Optional<Integer> min = numbers.stream()
                .min((x, y) -> (x < y) ? -1 : ((x == y) ? 0 : 1));

System.out.println(min.get()); // 1
  • 최소값을 구한다

7. max

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

Optional<Integer> max = numbers.stream()
                .max(Integer::compareTo);

System.out.println(max.get()); // 10
  • 최대값을 구한다

8-1. findFIrst

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

Optional<Integer> first = numbers.stream()
                .filter(n -> n > 5)
                .findFirst();

System.out.println("5보다 큰 첫 번째 숫자: " + any.get()); // 5보다 큰 첫 번째 숫자: 6
  • 순차적으로 돌면서 조건에 맞는 첫 번째 요소 찾기

8-2. findAny

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

Optional<Integer> any = numbers.stream()
                .filter(n -> n > 5)
                .findAny();

System.out.println("5보다 큰 아무 숫자: " + any.get()); // 5보다 큰 아무 숫자: 6
  • 스트림 조건에 맞는 아무 요소나 반환(순서와 관계 없음)
  • 이러한 특징 덕분에 병렬(Parallel) 스트림에서 가장 먼저 찾아지는 요소를 즉시 반환할 수 있어 성능상 이점이 있다

9-1. anyMatch

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

boolean hasEven = numbers.stream()
                .anyMatch(n -> n % 2 == 0);

System.out.println("짝수 존재: " + hasEven); // 짝수 존재: true
  • 스트림 요소 중 조건을 하나라도 만족

9-2. allMatch

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

boolean allPositive = numbers.stream()
                .allMatch(n -> n > 0);

System.out.println("모든 숫자 양수? " +  allPositive); // 모든 숫자 양수? true
  • 스트림 요소 중 모두가 만족

9-3. noneMatch

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

boolean noNegative = numbers.stream()
                .noneMatch(n -> n < 0);

System.out.println("음수 없나? " +  noNegative); // 음수 없나? true
  • 스트림 요소 중 아무도 만족하지 않은지를 boolean으로 반환

정리

  • 스트림의 중간 연산자들은 최종 연산이 호출되기 전까지 실행되지 않는다. 최종 연산이 실행되는 순간, 모든 중간 연산자들이 함께 동작하며 최종 결과를 만들어 낸다
  • 한 번 최종 연산을 거친 스트림은 닫히므로, 절대 재사용할 수 없다
  • reduce, findFirst, max 등 결과가 존재하지 않을 수 있는 연산은 항상 Optional로 반환한다

출처 – 인프런 강의 중 김영한의 실전 자바 – 고급 3편, 람다, 스트림, 함수형 프로그래밍