모던 자바 인 액션 - chapter 4, 5 스트림과 활용
스트림
스트림이란 무엇인가?
선언형으로 컬렉션 데이터를 처리할 수 있도록 자바 8 API에 추가된 기능이다. 또한 멀티스레드 코드를 구현하지 않아도 손쉽게 데이터를 병렬 처리할 수 있다.
스트림의 장점은 다음과 같다.
- 선언형으로 코드를 구현할 수 있다.
- 여러 블록연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
- 병렬 스트림을 사용하여 데이터 처리 속도를 높일 수 있다.
- 원본 데이터를 변경하지 않으므로 데이터의 불변성을 유지할 수 있다.
- 지연 연산을 통해 필요할 때만 연산을 수행하여 성능을 최적화할 수 있다.
** 지연 연산: filter, map과 같은 중간 연산은 collect와 같은 최종 연산이 호출되었을 때만 실행된다.
컬렉션과 스트림
컬렉션은 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료구조다. 즉, 컬렉션에 저장되기 위해서는 먼저 계산이 완료되어 있어야 한다. 반면 스트림은 이론적으로 요청할 때 요소를 계산하는 자료구조다.
스트림은 반복자와 마찬가지로 한번만 탐색할 수 있다. 탐색된 스트림의 요소는 소비된다.
내부 반복과 외부 반복
컬렉션 API를 사용하려면 사용자가 직접 요소를 반복해야 된다. for-each문이나 Iterator를 이용하여 반복하며 이를 외부 반복이라 한다. 반면 스트림 API는 반복을 알아서 처리하고 결과 스트림 값을 저장해주는 내부 반복을 사용한다.
내부 반복의 장점은 반복을 신경쓰지 않고 계산에만 몰두할 수 있다는 점과 병렬성을 쉽게 얻을 수 있다는 점이다.
중간 연산과 최종 연산
스트림은 연결할 수 있는 중간 연산과 스트림을 닫느 최종 연산으로 구성된다.
중간 연산은 스트림을 반환한다. 따라서 여러 중간 연산을 연결하여 파이프라인을 형성할 수 있다. 중간 연산의 가장 중요한 특징은 지연(lazy) 연산이다. 최종 연산이 호출되기 전까지는 아무런 연산을 수행하지 않는다.
List<Stirng> names = menu.stream() // 스트림 open
.filter(dish -> dish.getCalories > 300) // 중간 연산 시작
.map(Dish::getname)
.limit(3) // 중간 연산 끝, short-circuit
.collect(toList()); // 종단 연산
스트림의 게으른 특성 덕분에 얻을 수 있는 최적화 효과가 있다.
- 쇼트 서킷
모든 연산을 하기 전에 조건을 만족하면 이후의 불필요한 연산은 하지 않는다. - 루프 퓨전
둘 이상의 연산이 합쳐져 하나의 연산으로 처리된다.
최종 연산은 스트림 파이프라인에서 결과를 도출한다.
스트림 활용
중간 연산
필터링
- filter: Predicate를 통한 필터링
- distinct: 고유 요소만 필터링
스트림 슬라이싱
Predicate를 통한 슬라이싱
- takeWhile: Predicate이 처음으로 거짓이 되는 지점에 연산을 멈춘다.
- dropWhile: Predicate이 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.
스트림 축소
- limit: 처음 n개 요소를 갖는 스트림을 반환한다.
요소 건너뛰기
- skip: 처음 n개 요소를 제외한 스트림을 반환한다.
매핑
스트림의 각 요소에 함수 적용하기
- map: 함수를 인수로 받아 새로운 요소로 매핑된 스트림을 반환한다. 기본형 요소에 대한 mapToType 메서드도 지원한다(mapToInt, mapToLong, mapToDouble).
스트림 평면화
- flatMap: 제공된 함수를 각 요소에 적용하여 새로운 하나의 스트림으로 매핑한다.
최종 연산
검색과 매칭(쇼트 서킷)
- anyMatch: 적어도 한 요소와 일치하는지 확인하는 최종 연산(일치하는 순간 true 반환)
- allMatch: 모든 요소와 일치하는지 확인하는 최종 연산(일치하지 않는 순간 false 반환)
- noneMatch: 모든 요소가 일치하지 않는지 확인하는 최종 연산(일치하는 순간 false 반환)
- findFirst: 첫 번째 요소를 찾아 반환한다. 순서가 정해져 있을 때 사용한다.
- findAny: 요소를 찾으면 반환한다. 요소의 반환 순서가 상관없을 때 사용한다.
리듀싱
- reduce: 모든 스트림 요소를 BinaryOperator로 처리해서 값으로 도출한다.
- max, min: 요소에서 최대값 또는 최소값을 반환한다.
reduce 메서드의 장점과 병렬화
기존의 반복문을 통한 외부 반복은 합계를 구할 때 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어려웠다. reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게된다. 스트림은 내부적으로 포크/조인 을 통해 이를 처리한다.
스트림 연산: 상태
각각의 스트림 연산은 상태를 갖는 연산과 상태를 갖지 않는 연산으로 나뉘어져 있다.
map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보내는 함수적 역할만을 담당한다.
따라서 이들은 내부 상태를 갖지 않는 연산이다.
reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다.
sorted나 distinct 같은 연산은 스트림의 요소를 정렬하거나 중복을 제거하기 위해 과거의 이력을 알고 있어야 한다.
따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다. 이러한 연산을 내부 상태를 갖는 연산이라 한다.
숫자형 스트림
스트림 API는 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.
기본형 특화 스트림
IntStream, DoubleStream, LongStream이 존재하며 각각의 인터페이스에는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 메서드를 제공한다.
스트림으로 복원하기
boxed 메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.
Stream<Integer> boxed(); // in IntStream
숫자 범위
특정 범위의 숫자를 이용해야할 때 range와 rangeClosed 메서드를 사용할 수 있다. 이는 IntStream, LongStream 두 기본형 특화 스트림에서 지원된다. range는 열린 구간을 의미하며, rangeClosed는 닫힌 구간을 의미한다.
스트림 만들기
값으로 스트림 만들기
정적 메서드 Stream.of 를 이용하여 스트림을 만들 수 있다.
null이 될 수 있는 객체로 스트림 만들기
Stream.ofNullable 메서드를 이용하여 null이 될 수 있는 객체를 지원하는 스트림을 만들 수 있다.
배열로 스트림 만들기
Arrays.stream() 을 이용하여 스트림을 만들 수 있다.
파일로 스트림 만들기
파일을 처리하는 등의 I/O연산에 사용하는 자바의 NIO API도 스트림 API를 활용할 수 있도록 업데이트되었다. java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다.
함수로 무한 스트림 만들기
Stream.iterate와 Stream.generate를 통해 함수를 이용하여 무한 스트림을 만들 수 있다. iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다. 따라서 무제한으로 값을 계산할 수 있지만 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.
- Stream.iterate
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
- Stream.generate
public static<T> Stream<T> generate(Supplier<T> s)