stream이란 한마디로 데이터를 처리하는 통로이다.
스트림 자체가 추상적인 개념이기에 정의도 추상적일 수 밖에 없다.
보다 정확히는, java에서 스트림이란 공통화된 데이터 처리 인터페이스이다.
java에는 List, HashMap 등 여러가지 데이터 소스(타입)이 있다.
따라서, 기존에는 각 타입별로 다른 매서드로 데이터를 처리해야 했다.
그러나, 스트림의 등장으로 각 타입을 스트림으로 변환한 후,
스트림 매서드를 사용하여 공통의 매서드로 처리할 수 있게 되었다.
스트림에는 중간연산과 최종연산이 있다.
스트림을 쓸 때, 아래와 같이 여러가지 함수를 연결해 쓰는 경우가 많다.
myList.stream().distinct().limit(4).sorted().forEach(MyList::print);
여기서 .strea()은 myList 배열 소스로 하는 스트림을 생성한다.
이후, .distinct() 부터 sorted() 까지는 연산의 결과가 스트림이며, 뒤에 계속해서 연산을 덧붙일 수 있다. 이러한 연산들을 중간연산이라고 한다.
반면, forEach()는 최종연산 으로 스트림을 입력값으로 받아 파라미터로 받은 연산을 수행하며, 그 결과는 스트림이 아니다. 즉, 스트림의 요소들을 소모하므로 최종연산 이후에는 더이상 스트림 연산을 할 수 없다.
위에서 말했듯이, 스트림은 최종 연산이 끝나면 모든 요소를 소모한다.
따라서 스트림은 일회성이며, 재사용이 불가능하다.
예로 inputstrea()의 경우, 읽을 때 마다 바이트를 소비한다. 따라서 특정 파일의 내용을 담은 inputStream() 은 한 번만 읽을 수 있다.
위에 예시처럼 특정 파일로부터 stream을 생성해도, 원본 파일은 변경되지 않는다.
개인적으로 스트림의 가장 큰 장점이라고 생각한다.
스트림의 중간 연산들은, 마지막 최종 연산이 호출될 때 까지 호출되지 않는다. 아래 예시를 보자.
List<String> myArray = Arrays.of(["a", "b", "c", "d"])
List<String> filteredList = myArray
.map(String::toUpperCase)
.limit(1)
.toList();
// filterdList = {"A"}
해당 연산의 결과는 'A'만 가지고 있는 리스트가 될 것이다.
그런데 위 함수에서 map 은 전체 원소가 아니라 "A"에만 적용된다.
스트림에 익숙하면 오히려 이게 왜? 싶을 수 있다.
그런데 잘 생각해보자.
우리는 filterdList에 대해서 .map을 적용했다.
그렇지만 .map()은 적용된 순간에 바로 실행되지 않고, .toList()가 실행되는 순간까지 적용되지 않는다.(lazyLoading)
따라서, .toList()가 실행되서 .map()이 실행되는 순간, .limit() 가 있다는 걸 스트림은 알고 있고, 따라서 "a"에 스트림 연산이 적용된 후에는 나머지 원소에 대해서는 연산이 실행되지 않는다.
물론, for문을 사용해 중간에 return을 한다면 동일한 겨로가가 나오겠지만, 코드 가독성 측면에서 stream이 더 직관적이라고 생각한다.
이를 응용하면, DB를 이용해 대용량 데이터를 읽어올 때,
List<T> 에 저장한 후 처리한다면 모든 데이터가 메모리에 존재하지만,
스트림으로 읽어와 처리한다면 일정량만큼씩만 메모리에 올려서 처리할 수 있다.
따라서 대규모 데이터 처리에 스트림이 많이 쓰인다.
당연히 아니다.
개인적으로 사용하며 느꼈던 가장 큰 불편함은 중간에 return 이 불가능하다는 것이다.
또한, 단순한 for문이라면 오히려 forEach보다 성능이 좋다고 한다. forEach는 내부에서 for문을 돌리는 형태라서 코드는 깔끔하지만 단순 반복문의 경우에는 성능에 차이가 있는 것 같다.
또한, 스트림은 비교적 최근 기술이므로 상대적으로 최적화 부분이 덜 발달되어 있다고 한다.