[모던 자바인 액션] chp 5. 스트림 활용 (1)

sameul__choi·2022년 3월 20일
0

[모던 자바인 액션]

목록 보기
6/11
post-thumbnail

그래서 스트림을 어떻게 활용할 것인가 ? 이 장에서는 스트림 API 지원하는 다양한 연산을 살펴본다. 자바 8과 9에서 추가된 다양한 연산을 살펴본다.

스트림 API가 지원하는 연산을 이용하여 필터링, 슬라이싱, 매핑, 검색, 매칭, 리듀싱 등 다양한 데이터 처리 질의를 표현할 수 있다.

00 필터링

00절에서는 스트림 요소를 선택하는 방법, 즉 프레디케이트 필터링 방법과 고유 요소만 필터링 하는 방법을 배워보자

(1) 프레디케이트로 필터링

스트림 인터페이스는 filter 메서드를 지원한다. filter메서드는 프레디케이트(불리언 반환하는 함수)를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

List<Dish> vegetarianMenu = menu.stream()
								.filter(Dish::isVegetarian)
                                .collect(toList());

(2) 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 idstinct 메서드도 지원한다.(고유 여부는 객체의 hashCode, equals로 결정된다.) 예를 들어 다음 코드는 리스트의 모든 짝수를 선택하고 중복을 필터링 한다.

List<Dish> vegetarianMenu = menu.stream()
								.filter(i -> i % 2 ==0)
                      			.disticnt()                         
                                .forEach(System.out::printtln);

01 스트림 슬라이싱

01절에서는 스트림의 요소를 선택하거나 스킵하는 다양한 방법을 설명한다. 프레디케이트를 이용하는 방법, 스트림의 처음 몇 개의 요소를 무시하는 방, 특정 크기로 스트림을 줄이는 방법 등 다양한 방법을 이용해 효율쩍으로 이런 작업을 수행할 수 있다.

(1) 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가 처음으로 거짓이 되는 시점까지 발견된 요소를 버린다.

(2) 스트림 축소(limit), 요소 건너뛰기(skip)

limit(n)을 통해서 주어진 값 n개의 요소를 가지는 스트림을 반환한다.

skip(n)은 처음 n개의 요소를 제외, 이후 요소 만을 포함하는 스트림을 반환한다.

limit와 skip은 결과적으로 상호보완적인 연산 수행

02 매핑

특정 객체에서 특정데이터를 선택하는 작업

ex. Dish객체를 요소로 가지고 있는 스트림 ⇒ 매핑 ⇒ Dish객체의 calories값을 요소로 가지는 스트림

(1) 스트림의 각 요소에 함수 적용하기

map메서드는 함수를 인수로 받는데 이때 제공받은 함수를 각 요소에 적용시켜서 나온 결과가 새로운 요소로 매핑된다.

map메서드의 출력스트림은 인수로 제공받은 함수의 리턴타입 T에 대한 Stream가 된다.

(2) 스트림 평면화

상황: ["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 이 최종 반환된다.

해결 방법. flatMap을 사용해서 하나의 단일 스트림으로 반환 받기

잘못된 방법2에서의 문제는 .map(Arrays.stream ) 로 반환받는 객체가 하나의 스트림이 아닌 String배열 개별마다 스트림을 생성한다. (요소별로 스트림에 생기기 때문에 .distinct() 메서드가 별 의미없어짐)

flatMap을 사용하면 요소별로 생성되는 스트림을 하나의 스트림으로 평면화(연결)해준다.

즉 반환받은 하나의 Stream스트림 객체에 .distinct()를 체이닝 함으로써 원했던 고유한 알파벳 리스트 ["H","e", "l", "o", "W", "r", "d"] 를 반환 받을 수 있다.

03 검색과 매칭

(1) Predicate를 이용한 요소 검사 (anyMatch, allMatch, noneMatch)

위의 3가지 메서드는 boolean을 반환하며 자바의 &&, ||와 같은 스트림의 쇼트서킷 기법

쇼트서킷은 &&, || 같이 앞 조건식의 결과에 따라 뒤 조건식의 실행여부를 결정하는 논리연산자

//- allMatch

  //스트림의 모든 요소가 주어진 Predicate과 일치하는지 여부 반환

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

//- anyMatch

  //스트림의 요소 중에서 Predicate과 일치하는 경우가 적어도 하나라도 있는지 여부 반환

// - noneMatch

  //allMatch와 반대되는 연산

(2) 요소 검색 (findAny, findFirst)

findAny

다른 스트림과 연결해서 사용하며 조건에 만족하는 스트림 요소가 하나도 있는 경우 이를 반환한다. 아래의 예시에서 사용된 Optional클래스는 null처리 등을 쉽게 할 수 있도록 자바 8 부터 제공하는 클래스.

Optional dish = menu.stream().filter(Dish::isVegetarian).findAny();

findFirst

스트림은 정렬된 경우, 리스트인 경우 등.. 논리적인 아이템 순서가 정해져 있을 수 있다. 이때 해당 스트림의 첫 번째 요소를 찾을때 사용한다.

04 리듀싱

여태까지 본 스트림의 최종 연산 메서드 (allMatch, forEach, findAny)는 boolean, void, Optional 객체를 반환했다. 리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출해야 한다.

ex. 메뉴의 모든 칼로리의 합계

(1) 요소의 합

int sum = numbers.stream().reduce(0, (a,b) -> a + b);

위의 예시는 스트림의 모든 요소의 합을 구할 수 있도록 reduce 메서드를 사용했다. 해당 메서드는 두 인수를 가지는데 첫 번째는 초기값, 두 번째는 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator이다.

자바 8에서는 Interger클래스에 두 숫자를 더하는 정적 sum 메서드를 제공하기 때문에 람다식 대신 Integer::sum을 넘겨도 된다.

reduce메서드는 초깃값 인자를 받지 않는 경우도 있는데 해당 케이스는 Optional로 감싼 객체를 반환함으로써 NPE 등을 예방한다.

(2) 최대값과 최소값

reduce메서드의 인자에 스트림의 두 요소를 에서 최대값, 최소값을 반환하는 람다만 제공하면 전체 요소에서 최대, 최소값을 구할 수 있다. (스트림의 모든 요소를 소비할 때까지 해당 람다식 반복)

  Optional<Integer> max = numbers.stream().reduce(Integer::max);

(3) reduce 메서드의 장점과 병렬화

foreach문을 이용한 외부반복을 통해서 예를 들어서 sum += i; 이렇게 합계를 구한다고 하면 이를 병렬적으로 처리하기 어렵다. 스레드 생성 후, 해당 작업을 나눠서 병렬로 처리한다고 할 때 스레드끼리 sum에 대한 공유가 이루어져야 하기 때문이다.

reduce는 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce 메서드를 실행할 수 있게된다.

parallelStream()을 사용해서 모든 스트림 요소를 병렬적으로 처리할 수 있지만 자원의 비효율이 일어날 수 있으므로 사용하기에 적합한 상황인지를 확인해야한다. (이후 챕터에서 설명)

(4) 스트림 연산에서의 stateless 개념

map, filter같은 메서드의 경우, 각 요소를 처리해서 결과를 출력스트림으로 보낸다. 즉 내부적으로 참고하거나 관리하는 내부 상태를 갖지 않는 연산 (stateless operation)이다.

reduce, sum, max 등의 연산은 계산 결과를 누적해나갈 내부 상태가 필요하다. 하지만 아무리 요소 수가 많아도 현재까지의 합 처럼 참고가 필요한 내부 상태의 크기가 한정되어있다.

제일 문제가 되는게 sorted, distinct같은 연산인데, 해당 연산은 스트림의 모든 요소가 버퍼에 추가되어 있어야 하므로 스트림의 크기가 크면 부하가 생길 수 있다. 이런 연산은 내부 상태를 갖는 연산 (stateful operation)이라고 한다.

0개의 댓글