5장: 스트림 활용
스트림이 지원하는 다양한 연산을 배워보자.
Last updated
스트림이 지원하는 다양한 연산을 배워보자.
Last updated
이번 장에서는 아래 메서드들을 차근차근 살펴본다. 🐳
filter 메서드는 Predicate(boolean을 반환하는 함수형 인터페이스)를 인수로 받아서 Predicate에서 true를 반환하는 요소만 포함하는 스트림을 반환한다.
고유 요소로 이루어진 스트림을 반환하는 distinct메서드도 지원한다.
객체의 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정한다. (equals & hashcode를 잘 정의해하도록 하자..🥲)
스트림의 요소를 선택하거나 스킵하는 방법
데이터가 정렬되어 있고, 특정 조건이 참이 나오면 반복 작업을 중단하려면 takeWhile()을 이용해 처리할 수 있다.
filter는 모든 데이터를 검사하며 true인 것만 다음으로 넘어가지만, takeWhile은 조건에 대해 true가 아니게 될 경우 바로 거기서 멈추게 된다.
아래 예제는 320 칼로리 미만인 메뉴만 슬라이싱한다.
takeWhile과 정반대로, 데이터가 정렬되어 있고, 특정 조건이 거짓이 나오면 반복 작업을 중단하려면 dropWhile()을 이용해 처리할 수 있다.
프레디케이트가 처음으로 거짓이 되는 지점까지 탐색한 후, 작업을 중단하고 해당 지점 이후의 탐색하지 않은 모든 요소를 반환한다.
아래 예제는 320 칼로리 이상인 메뉴만 슬라이싱한다.
주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 사용할 수 있다.
최대 n개의 요소만을 반환할 수 있다.
아래는 최대 3개의 요소만을 반환하도록 하는 코드이다.
처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 제공한다.
n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.
limit과 skip은 상호 보완적인 연산을 수행한다.
아래는 300칼로리 이상인 음식 중 처음 두 요리는 스킵하고 나머지 요리들만 반환하는 코드이다.
특정 객체에서 특정 데이터를 선택하기 위해 매핑 작업을 수행할 수 있다.
함수를 인수로 받는 map 메서드를 지원한다.
인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
다음은 Dish 리스트를 스트림으로 순회하며 Dish의 name 필드만 추출한 리스트를 반환하는 코드이다.
다른 map 메서드를 체이닝하는 것도 가능하다.
다음은 Dish 리스트를 스트림으로 순회하며 name필드의 길이를 추출한 리스트를 반환하는 코드이다.
flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다.
스트림의 각 값을 다른 스트림으로 만든 다음에, 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
아래 코드의 flatMap은 단어들을 모두 알파벳 단위로 떼어낸 후 얻는 여러 개의 String[] 배열들을 하나의 String Stream으로 모아 순회할 수 있도록 해준다.
Predicate가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 활용한다.
Predicate가 주어진 스트림의 모든 요소와 일치하는지 확인할 때 allMatch 메서드를 활용한다.
Predicate가 주어진 스트림의 모든 요소와 일치하지 않는지 확인할 때 noneMatch 메서드를 활용한다.
쇼트 서킷
하나라도 조건에 맞지 않는다면 나머지 표현식의 결과에 상관없이 전체 결과가 정해지기 때문에 전체 스트림을 처리하지 않아도 결과를 반환할 수 있는 것을 말한다.
allMatch, noneMatch, findFirst, findAny 등의 연산이 모두 이에 해당한다.
findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
다른 스트림 연산과 연결해서 사용할 수 있으며, 결과를 찾는 즉시 스트림을 종료한다.
findFirst 메서드를 사용할 경우 병렬 실행 시 첫 번째 요소를 찾기 어려우므로 보통 병렬으로 실행시킬 때 제약이 적은 findAny 메서드를 사용한다.
스트림은 첫번째 요소를 찾는 findFirst 메서드를 제공한다.
리스트 또는 정렬된 데이터로부터 생성된 스트림은 논리적인 아이템 순서가 정해져 있을 수 있다.
스트림의 최종연산 중 하나로 마지막 결과가 나올때까지 스트림의 모든 요소를 반복적으로 처리하는 과정
함수형 프로그래밍 언어 용어로는 이과정이 마치 종이를 작은 조각이 될때까지 반복해서 접는것과 비슷하다 하여 폴드(fold)라고 부른다.
리듀스의 파라미터
초깃값
스트림의 두 요소를 조합해 새로운 값을 만드는 BinaryOperator<T> 의 구현체(람다)
reduce를 사용하면 스트림이 하나의 값으로 줄어들 때 까지 각 요소를 반복해 조합하기 때문에, 애플리케이션의 반복된 패턴을 추상화 할 수 있다.
reduce를 사용해 스트림의 모든 요소들의 총합을 구해보자.
초깃값을 0으로 두고, 더하기 메서드를 파라미터로 넘기면 스트림에 총합만 남을때까지 더하는 연산이 수행된다.
리듀스 연산에 초깃값을 주지 않으면 스트림에 아무 요소도 없는 경우 값을 반환할 수 없기 때문에 Optional 객체로 감싼 결과를 반환한다.
최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.
스트림의 모든 요소를 소비할 때 까지 max() 혹은 min() 연산을 한다.
reduce 메서드와 병렬화
reduce를 이용하면 내부 반복이 추상화되면서 포크/조인 프레임워크를 활용해 병렬로 reduce를 실행할 수 있게 된다.
단계적 반복으로 합계를 구할때는 sum 변수를 공유해야 하는데, 이로 인해 동기화 이슈가 발생해 병렬화가 어렵다.
물론 병렬로 실행하기 위해서는 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구조여야 한다.
스트림 연산 : 상태 있음과 상태 없음
상태 없음 (stateless operation)
map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
상태 있음 (stateful operation)
reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하지만 이는 int, double 등과 같이 작은 값이며, 스트림에서 처리하는 요소 수와 관계없이 한정(bounded)되어있다.
반면 sorted나 distinct 같은 연산을 수행하기 위해서는 과거의 이력을 알고있어야 한다. 예를 들어 어떤요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다.
3장 내용과 비슷하게 오토박싱을 피하기 위해 primitive type을 위한 스트림을 제공한다.
int 요소에 특화된 IntStream, double 요소에 특화된 DoubleStream, long 요소에 특화된 LongStream을 제공한다.
mapToInt, mapToDouble, mapToLong 메서드를 사용하면 기본형 특화 스트림으로 변환해준다.
아래와 같이 mapToInt 메서드로 각 요리에서 모든 칼로리(Interger형식)를 추출한 다음에 IntStream을(Stream<Integer>가 아님) 반환한다. 스트림이 비어있으면 sum은 기본값 0을 반환한다.
숫자 스트림을 기본 스트림으로 변환하려면 boxed() 메서드를 사용하면 된다.
Optional을 Integer, String 등의 참조 형식으로 파라미터화할 수 있다.
OptionalInt, OptionalDouble, OptionalLoing 세 가지 기본형 특화 스트림 버전을 제공하여 숫자 스트림 메서드의 결과를 받을 수 있다.
IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다. 두 메서드 모두 시작값과 종료값을 인수로 가진다.
range 메서드는 시작값과 종료값이 결과에 포함되지 않는다.
rangeClosed 메서드는 시작값과 종료값이 결과에 포함된다.
1부터 100까지 피타고라스의 수를 구한다. (a*a + b*b = c*c인 경우의 수를 모두 구하기)
a, b를 iteration 돌면서 a*a + b*b 의 제곱근이 정수일 경우에만 int[a, b, c]를 스트림에 남겨둔다.
임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.
Stream.empty() 메서드는 스트림을 비운다.
ofNullable 메서드로 null이 될 수 있는 객체를 포함하는 스트림을 만들 수 있다.
nullable한 객체를 포함하는 스트림값과 flatMap을 함께 사용하는 상황에서 유용하게 사용할 수 있다.
배열을 인수로 받는 정적 메서드 Arrays.stream()을 이용해서 스트림을 만들 수 있다.
파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있다.
Files.lines로 파일의 각 행 요소를 반환하는 스트림을 얻을 수 있다.
Stream 인터페이스는 AutoCloseable 인터페이스를 구현하므로, try 블록 내의 자원은 자동으로 관리된다.
다음은 입력받은 파일에서 각 행의 단어들을 flatMap으로 하나의 스트림으로 평면화한 후, 고유한 단어가 몇개인지 반환하는 코드이다.
Stream.iterate와 Stream.generate를 이용해서 함수로부터 크기가 고정되지 않은 무한 스트림(unbounded stream)을 만들 수 있다.
초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다.
보통은 무한한 값을 출력하지 않도록 limit(n) 메서드와 함께 사용한다.
혹은 두 번째 인수로 Predicate를 받아 작업 중단의 기준으로 사용할 수도 있다.
filter 메서드는 언제 작업을 중단할 지 알릴 수 없기 때문에, takeWhile 메서드를 사용해야 한다.
iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않으며, Supplier<T>를 인수로 받아서 새로운 값을 생산한다.
아래 코드는 0에서 1 사이의 임의의 double number 5개를 만든다.
generate 메서드의 경우 Supplier에 상태를 가질 가능성이 높아지므로, 순수한 불변 상태를 유지하는 iterate 메서드에 비해 병렬 환경에서 불안정하다.