•
java.util.stream.Stream 인터페이스는 많은 연산을 정의하고 있다. 스트림 인터페이스의 연산을 크게 두 가지로 구분할 수 있는데, 다음은 이전에 등장했던 예제다.
List<String> names = menu.stream()
.filter(dish -> dish.getCalories() > 300)
.map(Dish::getName)
.limit(3)
.collect(toList());
Java
복사
◦
위 예제에서 연산을 두 그룹으로 구분할 수 있다.
▪
filter, map, limit은 서로 연결되어 파이프라인을 형성한다.
▪
collect는 파이프라인을 실행한 다음에 종료한다.
◦
연결할 수 있는 스트림 연산을 중간 연산, 스트림을 닫는 연산을 최종 연산이라고 한다.
1. 중간 연산
•
filter나 sorted와 같은 중간 연산은 다른 스트림을 반환한다. 따라서 여러 중간 연산을 연결해서 질의를 만들 수 있다.
•
중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 것, 즉 게으르다는 것이다.
•
중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한 번에 처리하기 때문이다.
•
스트림 파이프라인에서 어떤 일이 일어나는지 쉽게 확인할 수 있도록 람다가 현재 처리 중인 요리를 출력해보자.
List<String> names = menu.stream()
.filter(dish -> {
System.out.println("filtering:" + dish.getName());
return dish.getCalories() > 300;
})
.map(dish -> {
System.out.println("mapping:" + dish.getName());
return dish.getName();
})
.limit(3)
.collect(toList());
System.out.println(names);
Java
복사
◦
다음은 프로그램 실행 결과다.
filtering:pork
mapping:pork
filtering:beef
mapping:beef
filtering:chicken
mapping:chicken
[pork, beef, chicken]
Java
복사
◦
스트림의 게으른 특성 덕분에 몇 가지 최적화 효과를 얻을 수 있었다.
▪
첫째, 300 칼로리가 넘는 요리는 여러 개지만 오직 처음 3개만 선택되었다. 이는 limit 연산 그리고 쇼트서킷이라 불리는 기법 덕분이다.
▪
둘째, filter와 map은 서로 다른 연산이지만 한 과정으로 병합되었다. 이 기법은 루프 퓨전이라고 한다.
2. 최종 연산
•
최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.
•
예를 들어 다음 파이프라인의 forEach는 소스의 각 요리에 람다를 적용하고 void를 반환하는 최종 연산이다. System.out.println을 forEach에 넘겨주면 menu에서 만든 스트림의 모든 요리를 출력한다.
menu.stream().forEach(System.out::println);
Java
복사
3. 스트림 이용하기
•
스트림 이용 과정은 다음과 같이 세 가지로 요약할 수 있다.
◦
질의를 수행할 데이터 소스
◦
스트림 파이프라인을 구성할 중간 연산 연결
◦
스트림 파이프라인을 실행하고 결과를 만들 최종 연산
•
스트림 파이프라인의 개념은 빌더 패턴과 비슷하다.
◦
빌더 패턴에서는 호출을 연결해서 설정을 만들고(중간 연산) 준비된 설정에 build 메서드를 호출한다.(최종 연산)