[Keyword][Java] Stream
Keyword
하루에 하나씩 키워드를 정해 공부해보려고 한다.
일단은 왜 이 키워드를 공부하려고 하는가(why), 그래서 이 키워드가 무엇인가(what), 어떻게 적용해나갈 것인가(how)로 나눠 정리하려고 한다!
Why?
마찬가지로 프로젝트 리뷰하면서 야호가 추천해 준 키워드이다. 블로그에 야호 지분이 80퍼센트 정도 되는 것 같고요..
왜 스트림을 사용해야 하냐면 가독성과 코드의 재사용을 위해서!
자바에서는 많은 양의 데이터를 저장하기 위해서 배열이나 컬렉션을 사용하는데, 반복문이나 반복자(iterator)를 사용하여 코드를 작성하는 건 길이가 너무 길고 가독성도 떨어지며, 코드의 재사용이 거의 불가능하다. 이러한 문제점을 극복하기 위해서 Java SE 8부터 스트림(stream) API를 도입했는데, 스트림 API는 데이터를 추상화하여 다루므로 다양한 방식으로 저장된 데이터를 읽고 쓰기 위한 공통된 방법을 제공한다. 따라서 스트림 API를 이용하면 배열이나 컬렉션뿐만 아니라 파일에 저장된 데이터도 모두 같은 방법으로 다룰 수 있게 된다. - TCPschool
What?
특징
스트림 API는 다음과 같은 특징을 가진다.
스트림은 외부 반복을 통해 작업하는 컬렉션과는 달리 내부 반복(internal iteration) 을 통해 작업을 수행한다.
스트림은 재사용이 가능한 컬렉션과는 달리 단 한 번만 사용할 수 있다.
스트림은 원본 데이터를 변경하지 않는다.
스트림의 연산은 필터-맵(filter-map) 기반의 API를 사용 하여 지연(lazy) 연산을 통해 성능을 최적화한다.
스트림은 parallelStream() 메소드를 통한 손쉬운 병렬 처리를 지원합니다.
생성
스트림 API는 다음과 같은 데이터 소스에서 생성할 수 있으며 각 생성 예제는 tcpschool를 참고하자.
- 컬렉션
- 배열
- 가변 매개변수
- 지정된 범위의 연속된 정수
- 특정 타입의 난수들
- 람다 표현식
- 파일
- 빈 스트림
Collection 인터페이스에 stream() 메소드가 정의되어 있으며, parallelStream() 메소드를 사용하면 병렬 처리가 가능한 스트림을 생성할 수 있다.
중개 연산(intermediate operation)
초기 생성된 스트림은 중개 연산을 통해 또 다른 스트림으로 변환된다. 이러한 중개 연산은 스트림을 전달받아 스트림을 반환하므로, 중개 연산은 연속으로 연결해서 사용할 수 있고, 스트림의 중개 연산은 필터-맵(filter-map) 기반의 API를 사용하므로 지연(lazy) 연산을 통해 성능을 최적화할 수 있다.
대표적인 중개 연산과 메소드는 다음과 같다.
중개연산 | 메소드, 설명 |
---|---|
스트림 필터링 | filter(): 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환 distinct(): 해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환, Object 클래스의 equals() 메소드를 사용하여 요소의 중복을 비교 |
스트림 변환 | map(): 해당 스트림의 요소들을 주어진 함수에 인수로 전달하여, 그 반환값들로 이루어진 새로운 스트림을 반환 flatMap(): 스트림의 요소가 배열인 경우, 각 배열의 각 요소의 반환값을 하나로 합친 새로운 스트림 반환 |
스트림 제한 | limit(): 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소만으로 이루어진 새로운 스트림을 반환 skip(): 해당 스트림의 첫 번째 요소부터 전달된 개수만큼의 요소를 제외한 나머지 요소만으로 이루어진 새로운 스트림을 반환 |
스트림 정렬 | sorted(): 주어진 비교자(comparator)를 이용해 정렬, 비교자를 전달하지 않으면 사전 편찬 순(natural order)으로 정렬 |
스트림 연산 결과 확인 | peek(): 결과 스트림으로부터 요소를 소모하여 추가로 명시된 동작을 수행, 주로 연산과 연산 사이에 결과를 확인하고 싶을 때 사용 |
최종 연산(terminal operation)
지연(lazy)되었던 모든 중개 연산들이 최종 연산 시에 모두 수행되며, 최종 연산 시에 모든 요소를 소모한 해당 스트림은 더는 사용할 수 없다.
최종 연산 | 메소드, 설명 |
---|---|
요소의 출력 | forEach(): 스트림의 각 요소를 소모하여 명시된 동작을 수행, 반환 타입이 void이므로 보통 스트림의 모든 요소를 출력하는 용도로 많이 사용 |
요소의 소모 | reduce(): 해당 스트림의 모든 요소를 소모하여 연산을 수행하고, 그 결과를 반환. 비어 있는 스트림과 reduce 연산을 할 경우 전달받은 초깃값을 그대로 반환 |
요소의 검색 | findFirst() , findAny(): 해당 스트림에서 첫 번째 요소를 참조하는 Optional 객체를 반환, 비어 있는 스트림에서는 비어있는 Optional 객체를 반환. 병렬 스트림인 경우에는 findAny() 메소드를 사용해야만 정확한 연산 결과를 반환 |
요소의 검사 | anyMatch(): 해당 스트림의 일부 요소가 특정 조건을 만족할 경우에 true를 반환 allMatch(): 해당 스트림의 모든 요소가 특정 조건을 만족할 경우에 true를 반환 noneMatch(): 해당 스트림의 모든 요소가 특정 조건을 만족하지 않을 경우에 true를 반환. 인수로 Predicate 객체를 전달받으며, 결과는 boolean 값으로 반환 |
요소의 통계 | count(): 해당 스트림의 요소의 총 개수를 long 타입의 값으로 반환 min() , max(): 해당 스트림의 요소 중에서 가장 큰 값과 가장 작은 값을 가지는 요소를 참조하는 Optional 객체 반환 |
요소의 연산 | sum() , average(): IntStream이나 DoubleStream과 같은 기본 타입 스트림에는 해당 스트림의 모든 요소에 대해 합과 평균을 구할 수 있는 sum()과 average() 메소드가 각각 정의되어 있으며, average() 메소드는 각 기본 타입으로 래핑된 Optional 객체를 반환 |
요소의 수집 | collect(): 인수로 전달되는 Collectors 객체에 구현된 방법대로 스트림의 요소를 수집. 수집 용도별 Collectors 메소드는 다음과 같다. 1. 스트림을 배열이나 컬렉션으로 변환 : toArray(), toCollection(), toList(), toSet(), toMap() 2. 요소의 통계와 연산 메소드와 같은 동작을 수행 : counting(), maxBy(), minBy(), summingInt(), averagingInt() 등 3. 요소의 소모와 같은 동작을 수행 : reducing(), joining() 4. 요소의 그룹화와 분할 : groupingBy(), partitioningBy() |
이 부분 역시 tcpschool 의 내용을 발췌했으며, 사용법과 사용 예제는 정말 많은 곳에서 보기 쉽게 정리되어 있다.
How?
정보 전달의 목적보다는 스스로의 공부에 목적이 있으므로 사용 에제와 방법을 여러 곳에서 발췌해 작성하는 것보다 직접 수행해보는 것이 의미가 있을 것 같다.
스트림이 무엇이며 어떻게 사용하는지에 대해 개략적으로 살펴보았으니, 프리온보딩 사전과제의 코드를 Stream을 이용해 리펙터링하며 직접 부딪혀 봐야겠다.
공부하며 추가적으로 알게 된 내용과 정리가 필요한 내용이 있다면 적어 둘 예정이며, 리펙터링은 프로젝트 회고에서 다룰 예정이다.