- 스트림을 이용하면 선언형 (즉, 데이터를 처리하는 임시 구현 코드 대신 질의로 표현할 수 있다) 으로 컬렉션 데이터를 처리할 수 있다.
- 스트림의 새로운 기능은 다음과 같은 다양한 이득을 가져다 준다.
- 선언형으로 코드를 구현할 수 있다.
- 즉, 루프와 if 조건문 등의 제어 블록을 사용해서 어떻게 동작을 구현할지 지정할 필요없이 선언형 방식으로 동작의 수행을 지정할 수 있다.
- 선언형 코드와 동작 파라미터화를 활용하면 변하는 요구사항에 쉽게 대응할 수 있다.
- 즉, 기존 코드를 복사하여 붙여 넣는 방식을 사용하지 않고 람다 표현식을 이용하여 특정 조건의 데이터를 필터링하는 코드도 쉽게 구현할 수 있다.
- 이러한 선언형 방식은 우리에게 ‘간결한 코드’와 ‘코드의 재사용성’, ‘코드의 가독성’ 등의 이점을 가져다준다.
- Filter, sorted, map, collect 같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
- 여러 연산을 파이프라인으로 연결해도 여전히 가독성/명확성이 유지된다.
- filter 메서드의 결과는 sorted 메서드로, 다시 sorted의 결과는 map 메서드로, map 메서드의 결과는 collect로 연결이 된다.
- Filter (또는 sorted, map, collect) 같은 연산은 고차원 빌딩 블록으로 이루어져 있으며, 특정 스레딩 모델에 제한되지 않고 자유룝게 사용가능하다.
- 결과적으로 데이터 처리과정을 병렬화 하면서 스레드와 락 걱정을 하지 않아도 된다.
- 선언형으로 코드를 구현할 수 있다.
- 스트림 API는 매우 비싼 연산이다. 일반적인 명령형 프로그래밍의 루프를 이용한다면 더 많은 시간을 들였을 것이다.
- 자바 8의 스트림 API의 특징은 다음처럼 요약할 수 있다.
- 선언형 : 더 간결하고 가독성이 좋아진다
- 조립가능 : 유연성, 재사용성이 좋아진다.
- 병렬화: 성능이 좋아진다.
- 자바 8의 컬렉션에서 스트림을 반환하는 stream 메서드가 추가되었다.
- 스트림이란 ‘데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소’ 로 정의할 수 있다.
- 연속된 요소
- 컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공한다.
- 컬렉션은 자료구조이므로 컬렉션에서는 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산이 주를 이룬다.
- 반면 스트림은 filter, sorted, map 처럼 표현 계산식이 주를 이룬다.
- 즉, 컬렉션의 주제는 데이터이고 스트림의 주제는 계산이다.
- 소스
- 스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비(consume) 한다.
- 그렇기 때문에, 정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지된다.
- 즉 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다.
- 데이터 처리 연산
- 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 비슷한 연산을 지원한다.
- 예를 들어, filter, map, reduce, find, match, sort 등으로 데이터를 조작할 수 있다.
- 또한, 순차적, 병렬적으로 실행도 할 수 있다.
- 파이프라이닝
- 대부분의 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다.
- 덕분에 laziness, short-circuiting 같은 최적화도 얻을 수 있다.
- 연산 파이프라인은 데이터 소스에 적용하는 데이터베이스 질의와 비슷하다.
- 내부 반복
- 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다.
- 연속된 요소
import static java.util.stream.Collectors.toList;
List<String> threeHighCaloricDishNames =
menu.stream() //메뉴에서 스트림을 얻는다.
.filter(d -> d.getCalories() > 300) //파이프라인 연산 만들기. 고 칼로리 요리 필터링
.map(Dish::getName) // 요리명 추출
.limit(3) //선착순 3개만 선택
.collect(toList()); //결과를 다른 리스트로 저장
- 위의 예제를 살펴보자.
- 먼저 데이터 소스는 menu 이다. 데이터 소스는 연속된 요소를 스트림에 제공한다.
- 다음으로 스트림에 filter, map, limit, collect로 이어지는 일련의 데이터 처리 연산을 적용한다.
- collect를 제외한 모든 연산은 서로 파이프라인을 형성할 수 있도록 스트림을 반환한다.
- 파이프라인은 소스에 적용하는
질의
같은 존재다. - 마지막으로 collect 연산으로 파이프라인을 처리해서 결과를 반환한다.
- 마지막에 collect를 호출하기 전까지는 menu에서 아무것도 선택되지 않으며, 출력 결과도 없다.
- 즉 collect가 호출되기 전까지 메서드 호출이 저장되는 효과가 있다.
- Collect : 스트림을 다른 형식으로 변환한다. Collect 가 다양한 변환 방법을 인수로 받아 스트림에 누적된 요소를 특정 결과로 변환시키는 기능을 수행하는 정도로 이해하자.
- 자바의 기존 컬렉션과 새로운 스트림 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
- 여기서 연속된이라는 표현은 순서와 상관없이 아무 값에나 접근 하는게 아니라, 순차적으로 값에 접근한다는 것을 의미한다.
- 데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이 라고 할 수 있겠다.
- 컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장 하는 자료구조다.
- 즉, 컬렉션의 모든 요소는 추가하기 전에 계산되어야 한다. (Eager Evaluation)
- eager evaluation 이기 때문에, 컬렉션에 요소를 추가하거나, 컬렉션의 요소를 삭제 할 수 있다
- 이런 연산을 수행할 때 마다, 컬렉션의 모든 요소를 메모리에 저장해야 하며, 컬렉션에 추가하려는 요소는 미리 계산되어야 한다.
- 즉, process의 code block에 있는 것이 직접적으로 메모리에 적재가 되는 것이다.
- 반면, 스트림은 이론적으로 요쳥할 때만 요소를 계산하는 고정된 자료구조다 (lazy evaluation)
- 스트림에 요소를 추가하거나 스트림에서 요소를 제거할 수 없다.
- 이러한 스트림의 특성은 프로그래밍에 큰 도움을 준다. 사용자가 요청하는 값만 스트림에서 추출한다는 것이 핵심이다.
- 스트림은 lazy 하기 때문에, 데이터를 요청할 때만 값을 계산한다. 이런 특징을
요청 중심 제조 (demand-driven manufacturing) 또는
just-in-time-menuefacturing` 이라고 부른다. - 컬렉션은 eager evaluation 이기 때문에, 적극적으로 계산된다. (생산자 중심 supplier-driven : 팔기도 전에 창고를 가득 채움)
- java.util.stream.Stream 인터페이스는 많은 연산을 정의한다.
- 스트림 인터페이스 연산을 크게 두가지로 구분할 수 있다.
- filter, map, limit는 서로 연결되어 파이프라인을 형성한다.
- collect로 파이프 라인을 실행한 다음에 닫는다
- 연결할 수 있는 스트림 연산을
intermediate operation
이라고 하며, 스트림을 닫는 연산을terminal operation
이라고 한다. intermediate operation : 중간연산
- filter나 sorted 같은 중간 연산은 다른 스트림을 반환한다.
- 따라서 중간 연산을 연결해서 질의를 만들 수 있다.
- 중간 연산의 중요한 특징은 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다는 점이다. (lazy evaluation)
- 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한번에 처리하기 때문이다.
- 이러한 lazy 한 평가 덕분에 컴파일 타임에 최적화가 가능하다.
terminal operation : 최종연산
- 최종 연산은 스트림 파이프라인에서 결과를 도출한다.
- 보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.
- 예를 들어
foreach
는 소스의 각 요리에 람다를 적용한 다음에 void를 반환하는최종 연산
이다.- `menu.stream().foreach(System.out::println);
- 스트림 이용과정은 다음과 같이 세가지로 요약할 수 있다.
- 질의를 수행할 (컬렉션 같은) 데이터 소스
- 스트림 파이프라인을 구성할 중간 연산
- 스트림 파이프라인을 실행하고 결과를 만들 최종 연산
- 스트림 파이프라인의 개념은 builder pattern 과 비슷하다. 빌더 패턴에서는 호출을 연결해서 설정을 만든다. 그리고 준비된 설정에 build 메서드를 호출한다.
- 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.
- 스트림은 내부 반복을 지원한다. 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화한다.
- 스트림에는 중간 연산과 최종 연산이 있다.