Chapter 5. 스트림 활용
ToC
- 필터링, 슬라이싱, 매칭
- 검색, 매칭, 리듀싱
- 특정 범위의 숫자와 같은 숫자 스트림 사용하기
- 다중 소스로부터 스트림 만들기
- 무한스트림
5.0.
-
이전 챕터(4)에서 봤던 스트림의 특징
-
연산 파이프라인을 명시적으로 구성할 수 있다.
-
컬렉션 처리를 명시적인 반복(외부반복)대신 스트림API가 관리(내부반복)하므로 개발자는 반복문 로직, 병렬처리에 대한 신경을 덜 쓸 수 있다.
-
5.1. 필터링
스트림의 요소를 선택하는 방법
-
Predicate을 이용한 필터링
-
고유 요소 필터링
- Predicate을 이용한 필터링(filter)
스트림 인터페이스가 지원하는 filter 메서드는 Predicate(boolean반환 함수)을 받아서 이에 부합하는 요소를 포함하는 스트림을 반환한다.
List vegetarianMenu = menu.stream() .filter(Dish::isVegetarian) .collect(toList());
- 고유 요소 필터링(distinct)
중복요소가 있는 경우, 고유한 요소로 이루어진 스트림을 반환
5.2 스트림 슬라이싱 (자바9부터 지원)
-
Predicate을 이용한 슬라이싱(takeWhile, dropWhile)
List 요소 중에서 칼로리가 300이하인 요소만 선택하려면?
위에 사용한 filter()를 이용해서 filter(dish -> dish.getCalories() < 300 ) 다음과 같이 조건을 주면 된다. 하지만 이미 정렬되어 있는 리스트의 경우라면 300이상의 칼로리인 요소가 나온 시점부터는 반복을 중단해도 된다. 이런 케이스에 takeWhile을 사용한다.
List slicedMenu = menu.stream() .takeWhile(dish -> dish.getCalories() < 300) .collect(toList());
위의 takeWhile과 정반대되는 작업을 하는 dropWhile도 있다. dropWhile은 Predicate가 처음으로 거짓이 되는 시점까지 발견된 요소를 버린다.
-
스트림 축소(limit), 요소 건너뛰기(skip)
limit(n)을 통해서 주어진 값 n개의 요소를 가지는 스트림을 반환한다.
skip(n)은 처음 n개의 요소를 제외, 이후 요소 만을 포함하는 스트림을 반환한다.
limit와 skip은 결과적으로 상호보완적인 연산 수행
5.3 매핑
특정 객체에서 특정데이터를 선택하는 작업
ex. Dish객체를 요소로 가지고 있는 스트림 ⇒ 매핑 ⇒ Dish객체의 calories값을 요소로 가지는 스트림
-
스트림의 각 요소에 함수 적용하기
map메서드는 함수를 인수로 받는데 이때 제공받은 함수를 각 요소에 적용시켜서 나온 결과가 새로운 요소로 매핑된다.
map메서드의 출력스트림은 인수로 제공받은 함수의 리턴타입 T에 대한 Stream가 된다.
-
스트림 평면화
상황: [
"Hello"
,"World"
] 리스트에서 해당 문자열들을 이루는 고유한 알파벳을 포함하는 리스트
["H"
,"e"
, "l"
, "o"
, "W"
, "r"
, "d"
] 를 반환받고 싶다.
잘못된 방법1. map 메서드를 사용해서 단어를 단일문자로 매핑하고 distinct를 걸어준다면..?
words.stream().map(word -> word.split("")) //단일문자 매핑
.distinct().collect(toList()); //distinct
여기서 문제는 map메서드가 반환하는 타입이 Stream이 아닌 Stream<String[]>이라는 것이다. split() 함수의 리턴타입이 String[]이어서 그렇다.
잘못된 방법2. map과 Arrays.stream 활용
Arrays.stream() 메서드는 String배열을 인자로 받아서 Stream으로 만들어준다.
하지만 map메서드를 통해서 반환받은 스트림객체에 .map(Arrays::stream)
메서드를 추가하면 String배열마다 별도의 스트림을 생성하게 되므로 결과적으로 List<Stream> 이 최종 반환된다.
해결 방법. flatMap을 사용해서 하나의 단일 스트림으로 반환 받기
잘못된 방법2에서의 문제는 .map(Arrays.stream ) 로 반환받는 객체가 하나의 스트림이 아닌 String배열 개별마다 스트림을 생성한다. (요소별로 스트림에 생기기 때문에 .distinct() 메서드가 별 의미없어짐)
flatMap을 사용하면 요소별로 생성되는 스트림을 하나의 스트림으로 평면화(연결)해준다.
즉 반환받은 하나의 Stream스트림 객체에 .distinct()를 체이닝 함으로써 원했던 고유한 알파벳 리스트 ["H"
,"e"
, "l"
, "o"
, "W"
, "r"
, "d"
] 를 반환 받을 수 있다.
5.4. 검색과 매칭
* Predicate를 이용한 요소 검사 (anyMatch, allMatch, noneMatch)
위의 3가지 메서드는 boolean을 반환하며 자바의 &&, ||와 같은 스트림의 쇼트서킷 기법
쇼트서킷은 &&, || 같이 앞 조건식의 결과에 따라 뒤 조건식의 실행여부를 결정하는 논리연산자
- allMatch
스트림의 모든 요소가 주어진 Predicate과 일치하는지 여부 반환
boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);
- anyMatch
스트림의 요소 중에서 Predicate과 일치하는 경우가 적어도 하나라도 있는지 여부 반환
- noneMatch
allMatch와 반대되는 연산
* 요소 검색 (findAny, findFirst)
-
findAny
다른 스트림과 연결해서 사용하며 조건에 만족하는 스트림 요소가 하나도 있는 경우 이를 반환한다. 아래의 예시에서 사용된 Optional클래스는 null처리 등을 쉽게 할 수 있도록 자바 8 부터 제공하는 클래스.
Optional dish = menu.stream().filter(Dish::isVegetarian).findAny();
-
findFirst
스트림은 정렬된 경우, 리스트인 경우 등.. 논리적인 아이템 순서가 정해져 있을 수 있다. 이때 해당 스트림의 첫 번째 요소를 찾을때 사용한다.
5.5. 리듀싱
여태까지 본 스트림의 최종 연산 메서드 (allMatch, forEach, findAny)는 boolean, void, Optional 객체를 반환했다. 리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출해야 한다.
ex. 메뉴의 모든 칼로리의 합계
* 요소의 합
int sum = numbers.stream().reduce(0, (a,b) -> a + b);
위의 예시는 스트림의 모든 요소의 합을 구할 수 있도록 reduce 메서드를 사용했다. 해당 메서드는 두 인수를 가지는데 첫 번째는 초기값, 두 번째는 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator이다.
자바 8에서는 Interger클래스에 두 숫자를 더하는 정적 sum 메서드를 제공하기 때문에 람다식 대신 Integer::sum을 넘겨도 된다.
reduce메서드는 초깃값 인자를 받지 않는 경우도 있는데 해당 케이스는 Optional로 감싼 객체를 반환함으로써 NPE 등을 예방한다.
* 최대값과 최소값
reduce메서드의 인자에 스트림의 두 요소를 에서 최대값, 최소값을 반환하는 람다만 제공하면 전체 요소에서 최대, 최소값을 구할 수 있다. (스트림의 모든 요소를 소비할 때까지 해당 람다식 반복)
Optional<Integer> max = numbers.stream().reduce(Integer::max);
* reduce 메서드의 장점과 병렬화
foreach문을 이용한 외부반복을 통해서 예를 들어서 sum += i;
이렇게 합계를 구한다고 하면 이를 병렬적으로 처리하기 어렵다. 스레드 생성 후, 해당 작업을 나눠서 병렬로 처리한다고 할 때 스레드끼리 sum에 대한 공유가 이루어져야 하기 때문이다.
reduce는 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce 메서드를 실행할 수 있게된다.
parallelStream()을 사용해서 모든 스트림 요소를 병렬적으로 처리할 수 있지만 자원의 비효율이 일어날 수 있으므로 사용하기에 적합한 상황인지를 확인해야한다. (이후 챕터에서 설명)
* 스트림 연산에서의 stateless 개념
map, filter같은 메서드의 경우, 각 요소를 처리해서 결과를 출력스트림으로 보낸다. 즉 내부적으로 참고하거나 관리하는 내부 상태를 갖지 않는 연산 (stateless operation)이다.
reduce, sum, max 등의 연산은 계산 결과를 누적해나갈 내부 상태가 필요하다. 하지만 아무리 요소 수가 많아도 현재까지의 합
처럼 참고가 필요한 내부 상태의 크기가 한정되어있다.
제일 문제가 되는게 sorted, distinct같은 연산인데, 해당 연산은 스트림의 모든 요소가 버퍼에 추가되어 있어야 하므로 스트림의 크기가 크면 부하가 생길 수 있다. 이런 연산은 내부 상태를 갖는 연산 (stateful operation)이라고 한다.
5.7. 숫자형 스트림
이전에 봤던 reduce 메서드를 사용한 스트림 요소 전체합 계산 시, Integer를 기본현으로 언박싱하는 비용이 든다. 이를 보완하기 위해서 스트림 API는 기본형 특화 스트림
을 제공한다.
* 숫자 스트림으로 매핑
mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다. map과 기능적으로 같지만 반환하는 타입이 Stream가 아닌 특화된 스트림을 반환한다
int calories = menu.stream()
.mapToInt(Dish::getCalories) // IntStream반환
.sum();
Stream가 아닌 IntStream을 반환하고 IntStream인터페이스에서 제공하는 sum메서드를 이용해서 따로 언박싱하는 비용을 줄일 수 있다.
* 기본형 특화 스트림용 Optional 클래스
OptionalInt, OptionalDouble 등 숫자스트림 연산에 대한 return 값을 Optional 로 받을 수 있다.
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();
* 숫자 범위
IntStream과 LongStream에서 제공하는 정적 메서드 range, rangeClosed를 사용해서 특정 범위의 숫자를 숫자스트림으로 표현할 수 있다.
ex. IntStream.rangeClosed(1, 100)
5.8. 스트림 만들기
stream() 메서드를 통해서 컬렉션에서 스트림을 만드는 것 뿐 아니라 다양한 방식으로 스트림을 만들 수 있다.
* 값으로 스트림 만들기
Stream<String> stream = Stream.of("A","B","C","D");
Stream.empty()
를 이용해서 빈 스트림을 만들수도 있다.
값이 nullable한 경우, 값이 null이면 빈 스트림객체를 생성하도록 명시적으로 null체크를 해야 한다. 이런 경우에 Stream.ofNullable
을 이용한다.
* 배열로 스트림 만들기
배열을 인수로 받는 정적메서드 Arrays.stream()
메서드를 사용해서 스트림을 만들 수 있다.
int[] numbers = {1,2,3,4,5};
int sum = Arrays.stream(numbers).sum();
* 함수로 무한 스트림 만들기
여태까지 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과는 다르게 Stream.iterate
와 Stream.generate
을 이용하면 크기가 고정되지 않은 무한 스트림을 만들 수 있다.
보통 실제 사용할때는 limit 메서드와 같이 사용한다.
-
iterate 메서드
첫 번째 인수로 초기값, 두 번째 인수로 람다를 받아서 무한 스트림을 생성한다. 무한 스트림의 요소 전체에 영향을 주는 sort나 reduce등의 메서드를 수행할 수 없다. (계산이 무한적으로 반복되므로)
Stream.iterate(0, n -> n + 2).limit(10).collect(toList());
'자바' 카테고리의 다른 글
[모던 자바 인 액션_정리] Chapter 7. 병렬 데이터 처리와 성능 (0) | 2020.04.17 |
---|---|
[모던 자바 인 액션_정리] Chapter 6. 스트림으로 데이터 수집 (0) | 2020.04.12 |
[모던 자바 인 액션_정리] Chapter 4. 스트림 소개 (0) | 2020.04.05 |
[모던 자바 인 액션_정리] Chapter 3. 람다 표현식 (0) | 2020.04.02 |
[모던 자바 인 액션_정리] Chapter 2. 동작 파라미터화 코드 전달하기 (0) | 2020.03.30 |