김영한님의 김영한의 실전 자바 – 고급 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로 반환한다