Today I Learned

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

JAVA & Spring/모던 자바 인 액션

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

하이라이터 2021. 7. 7. 02:05
728x90

이 장의 내용

  • 필터링, 슬라이싱, 매칭
  • 검색, 매칭, 리듀싱
  • 특정 범위의 숫자와 같은 숫자 스트림 사용하기
  • 다중 소스로부터 스트림 만들기
  • 무한 스트림

이 장에서는 스트림 API가 지원하는 다양한 연산을 살펴본다.

5.1 필터링

5.1.1 프레디케이트로 필터링

filter 메서드는 predicate(불리언을 반환하는 함수)를 인수로 받아서 일치하는 모든 요소를 포함하는 스트림을 반환한다.

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

5.1.2 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드를 지원한다.

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
number.stream()
  .filter(i -> i % 2 == 0)
  .distinct()
  .forEach(System.out::println);


5.2 스트림 슬라이싱

5.2.1 프레디케이트를 이용한 슬라이싱

TALKWHILE 활용

자바9에서는 스트림의 요소를 효과적으로 선택할 수 있도록 talkWhile, dropWhile 메서드를 지원한다.

List<Dish> specialMenu = Arrays.asList(
  new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
  new Dish("prawns", false, 300, Dish.Type.FISH),
  new Dish("rice", true, 120, Dish.Type.OTHER),
  new Dish("chicken", false, 120, Dish.Type.MEAT),
  new Dish("french fires", true, 530, Dish.Type.OTHER));

위와 같이 정렬된 리스트가 있을때 filter 연산을 이용해 전체 스트림을 반복하는 대신,

talkWhile 연산을 이용해서 반복작업을 중단할 수 있다.

List<Dish> sliceMenu1 = specialMenu.stream()
  .talkWhile(dish -> dish.getCalories() < 320)
  .collect(toList()); //seasonal fruit, prawns

DROPWHILE 활용

dropWhile은 talkWhile과 반대로 프레디케이트가 거짓이 되는 지점까지 발견되는 요소를 버린다.

List<Dish> sliceMenu2 = specialMenu.stream()
  .dropWhile(dish -> dish.getCalories() < 320)
  .collect(toList()); //rice, chicken, french fries

5.2.2 스트림 축소

limit(n) 메서드를 사용하면 주어진 값 이하의 크기를 갖는 스트림을 반환한다.

List<Dish> dishes = specialMenu.stream()
  .filter(dish -> dish.getCalories() > 300)
  .limit(3)
  .collect(toList()); //rice, chicken, french fries

소스가 정렬되어 있지않다면 limit의 결과로 정렬되어있지 않은 상태로 반환된다.

5.2.3 요소 건너뛰기

skip(n) 메서드는 처음 n개 요소를 제외한 스트림을 반환한다.

List<Dish> dishes = specialMenu.stream()
  .filter(dish -> dish.getCalories() > 300)
  .skip(2)
  .collect(toList());

5.3 매핑

5.3.1 스트림의 각 요소에 함수 적용하기

map 메서드는 함수를 인수로 받는다. 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.

List<Dish> disheNames = specialMenu.stream()
  .map(Dish::getName)
  .collect(toList());

getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String> 형식을 갖는다.

 

각 요리명의 길이를 알고싶다면 어떻게 해야할까?

다음처럼 다른 map 메서드를 연결할 수 있다.

List<Dish> disheNames = specialMenu.stream()
  .map(Dish::getName)
  .map(String::length)
  .collect(toList());

5.3.2 스트림 평면화

["Hello", "World"] 리스트가 있을때 ["H", "e", "l", "o", "W", "r", "d"] 고유문자로만 이루어진 리스트를 반환하려면 어떻게 해야할까?

word.stream()
  .map(word -> word.split(""))
  .disctinct()
  .collect(toList());

 

위와같이 disctinct로 중복 문자를 필터링하기에는 map의 결과가 각 단어의 String[](문자열 배열)을 반환한다는 문제가 있다.

 

map과 Arrays.Stream 활용

배열 대신 문자열 스트림을 만들기 위해 문자열을 받아 스트림을 만드는 Arrays.stream() 메서드를 사용해보자.

words.stream()
  .map(word -> word.split("")) //각 단어를 개별 문자열로 반환
  .map(Arrays::stream) //각 배열을 별도의 스트림으로 생성
  .distinct()
  .collect(toList));

하지만 결과가 스트림 리스트(List<Stream<String>>) 형태로 만들어져 문제가 해결되지 않는다.

 

flatMap 사용

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다.

즉, 각각의 스트림을 모아 하나의 스트림으로 연결한다.

words.stream()
  .map(word -> word.split("")) //각 단어를 개별 문자열로 반환
  .flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화
  .distinct()
  .collect(toList));


5.4 검색과 매칭

5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인

anyMatch 메서드는 주어진 스트림에서 적어도 한 요소와 일치하는지 확인한다.

if (menu.stream.anyMatch(Dish::isVegetarian)) {
  System.out.println("the menu is (somewhat) vegetarian friendly!!");
}

anyMatch는 불리언을 반환하므로 최종연산이다.

5.4.2 프레디케이트가 모든 요소와 일치하는지 검사

allMatch는 anyMatch와 달리 스트림의 모든 요소가 일치하는지 확인한다.

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

NONEMATCH

noneMatch는 allMatch와 반대 연산을 수행한다. 즉 일치하는 요소가 하나도 없는지 확인한다.

boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000);

anyMatch, allMatch, noneMatch 메서드는 자바의 &&, ||과 같이 쇼트서킷 기법을 사용한다.

* 쇼트서킷 : 전체 연산을 수행하지 않았더라도 원하는 요소를 찾았으면 즉시 결과를 반환하는 기법

5.4.3 요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.

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

Optional이란?

Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.

위 예제에서 findAny는 아무 요소도 반환하지 않을 수 있기 때문에 null을 방지하고자 사용되었다.

 

Optional의 메소드

  • isPresent() : Optional이 값을 포함하면 true, 포함하지 않으면 false를 반환
  • isPresent(Consumer<T> b   lock) : 값이 있으면 주어진 블록을 실행
  • T get() : 값이 존재하면 반환, 없으면 NoSuchElementException을 발생
  • T orElse(T other) : 값이 있으면 반환, 없으면 기본값을 반환

5.4.4 첫 번째 요소 찾기

정렬된 연속 데이터로부터 생성된 스트림에는 논리적인 아이템 순서가 정해져있을 수 있다.

findFirst 메소드는 이런 스트림에서 첫 번째 요소를 찾을때 사용한다.

Optional<Integer> firstNumber = someNumbes.stream()
  .map(n -> n * n)
  .filter(n -> n % 3 == 0)
  .findFirst();

* 병렬 실행에서는 첫 번째 요소를 찾기가 힘들다. 따라서 요소 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.


5.5 리듀싱

리듀스 연산을 이용하면 '메뉴의 모든 칼로리의 합계를 구하시오', '메뉴에서 칼로리가 가장 높은 요리는?' 처럼 모든 스트림 요소를 반복적으로 처리해서 결과를 도출하는 작업을 수행할 수 있다.

함수형 프로그래밍 언어에서는 종이를 계속해서 접는것과 비슷하다고하여 폴드(fold)라 부른다.

 

reduce는 두 개의 인수를 갖는다.

  • 초깃값
  • 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다

5.5.1 요소의 합

먼저 for-each 루프를 이용해서 리스트의 숫자 요소를 더하는 코드를 확인해보자.

int sum = 0;
for ( int x : numbers ) {
  sum += x;
}

위 코드는 reduce를 사용해서 다음과 같이 변경할 수 있다.

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

메서드 참조로 Interger 클래스의 sum 메서드를 사용하면 더 간결하게 구현 가능하다.

int sum = numbers.stream().reduce(0, Integer::sum);

 

초깃값 없음

초깃값을 받지 않도록 오버로드된 reduce도 있다. 하지만 이 reduce는 Optional 객체를 반환한다.

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

스트림에 아무 요소도 없다면 초깃값이 없으므로 reduce는 합계를 반환할 수 없기 때문이다.

5.5.2 최댓값과 최솟값

reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다.

이를 통해 최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.

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

reduce 메서드의 장점과 병렬화

단계적 반복으로 합계를 구할때는 sum 변수를 공유해야 하므로 쉽게 병렬화가 어렵다.

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

물론 병렬로 실행하기 위해서는 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구조여야 한다.

 

스트림 연산 : 상태 있음과 상태 없음
스트림 연산은 각각 다양한 작업을 수행한다. 따라서 각각의 연산은 내부적인 상태를 고려해야한다.

map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
따라서 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다.

reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 하지만 내부 상태는 int, double 등과 같이 작은 값이며, 스트림에서 처리하는 요소 수와 관계없이 한정(bounded)되어있다.

반면 sorted나 distinct 같은 연산을 수행하기 위해서는 과거의 이력을 알고있어야 한다. 예를 들어 어떤요소를 출력스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다. 이러한 연산을 내부 상태를 갖는 연산(stateful operation)이라 한다.

 

728x90
Comments