Chap05. 스트림 활용

728x90

필터링

프레디케이트로 필터링

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

예제

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

고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다. (고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다).

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

스트림 슬라이싱

스트림의 요소를 선택하거나 스킵하는 다양한 방법을 설명한다.

TAKEWHILE 활용

takeWhile을 이용하면 무한스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스할 수 있다.

List<Dish> sliceMenu = specialMenu.stream()
                                  .takeWhile(dish -> dish.getCalories() < 320)
                                  .collect(toList());

DROPWHILE 활용

나머지 요소를 선택하려면 어떻게 해야 할까?
dropWhile을 이용해 이 작업을 완료할 수 있다.

List<Dish> sliceMenu = specialMenu.stream()
                                  .dropWhile(dish -> dish.getCalories() < 320)
                                  .collect(toList());

dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.

스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limin(n) 메서드를 지원한다.

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

요소 건너뛰기

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

매핑

특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다.
스트림 API의 map, flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.

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

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

List<String> dishNames = menu.stream()
                             .map(Dish::getName)
                             .collect(toList());

스트림 평면화

flatMap은 각 배열 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map(Arrays.stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.

image

검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다. 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.

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

프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용한다.
anyMatch는 boolean을 반환하므로 최종 연산이다.

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

allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.

NONEMATCH

noneMatch는 allMatch와 반대 연산을 수행한다. 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

anyMatch, allMatch, noneMatch 새 메서드들은 쇼트서킷연산을 활용한다.

요소 검색

findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용할 수 있다.

Optional은 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. null 처리를 위한 방식으로 자바 8에 추가되었다. 뒷장에서 배우니 생략한다.

Optional에는 아래의 기능들이 존재한다.

  • isPresent() : Optional이 값을 포함하면 true를 반환하고, 값을 포함하지 않으면 false를 반환한다.
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 블록을 실행한다.
  • T get() : 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
  • T orElse() : 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.

첫 번째 요소 찾기

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야할까?

findFirst, findAny는 언제 사용할까?

병렬 프로그래밍에서는 첫 번째 요소를 찾기가 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

리듀싱

리듀스 연산은 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 설명한다. 이러한 질의를 수행하려면 Integer 같은 결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리해야 한다. 이런 질의를 리듀싱 연산이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부른다.

요소의 합

int product = numbers.stream()
                     .reduce(1, (a,b) -> a * b);
int sum = numbers.stream()
                 .reduce(0, Integer::sum);

초깃값 없음

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

Optional<Integer> sum = numbers.stream()
                               .reduce((a, b) -> (a + b));

왜 Optional<Integer>를 받을까? 그 이유는 스트림에 아무 요소도 없는 상황을 가정했을 때, 이런 상황이라면 초깃값이 없으므로 합계를 반환할 수 없다. 따라서 합계가 없음을 가질 수 있도록 Optional 객체로 감싼 결과를 반환한다.

최댓값과 최솟값

최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다. reduce를 이용해서 스트림에서 최댓값과 최솟값을 찾는 방법을 살펴보자. reduce는 두 인수를 받는다.

  • 초깃값
  • 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다
Optional<Integer> max = numbers.stream()
                               .reduce(Integer::max);

Integer.max 대신 Integer.min을 reduce로 넘겨주면 최솟값을 찾을 수 있다.
(가급적이면 메서드 참조 표현을 사용해서 가독성을 높이자!)

숫자형 스트림

이전의 reduce로 연산을 하는 행위는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.
다행히도 스트림 API 숫자 스트림을 효율적으로 처리할 수 있는 기본형 특화 스트림을 제공한다.

기본형 특화 스트림

자바 8에서는 세 가지 기본형 특화 스트림을 제공한다. IntStream, DoubleStream, LongStream 을 제공한다. 각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. (오직 박싱 과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하지는 않는다)

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다. 이들 메서드는 map과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림(ex. IntStream..)을 반환한다.

객체 스트림으로 복원하기

boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

IntStream intStream = menu.stream()
                          .mapToInt(Dish::getCalories);
Stream<Integer> stream = IntStream.boxed();

숫자 범위

IntStream과 LongStream에서는 rangerangeClosed라는 두 가지 정적 메서드를 제공한다. 두 메서드는 모두 첫 번째 인수로 시작값을 두 번째 인수로 종료값을 갖는다. range는 끝을 포함하지 않고, rangeClosed는 끝을 포함한다.

스트림 만들기

다양한 방식으로 스트림을 만들 수 있다. 이 절에서는 일련의 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식으로 스트림을 만드는 방법을 설명한다.

값으로 스트림 만들기

임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.

Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

다음처럼 empty 메서드를 이용해서 스트림을 비울 수 있다.

Stream<String> emptyStream = Stream.empty();

null이 될 수 있는 객체로 스트림 만들기

자바 9에서는 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메서드가 추가되었다. 때로는 null이 될 수 있는 객체를 스트림으로 만들어야 할 수 있다. null이 나올 수 도 있는 값을 Stream.ofNullable( ) 로 감싸서 구현할 수 있다.

``java
Stream values = Stream.of("config", "home", "user")
.flatMap(key -> Stream.ofNullable(System.getProperty(key)));


### 배열로 스트림 만들기 

배열을 인수로 받는 정적 메서드 ``Arrays.stream``을 이용해서 스트림을 만들 수 있다. 예를 들어 다음처럼 기본형 int로 이루어진 배열을 IntStream으로 변환할 수 있다. 

```java
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum(); 

파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(Non Blocking Input/Output)도 스트림 API를 활용할 수 있도록 업데이트되었다. java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다. 예를 들어 Files.lines는 주어진 파일의 행 스트림을 문자열로 반환한다. 지금까지 배운 스트림 연산을 활용하면 다음 코드처럼 파일에서 고유한 단어 수를 찾는 프로그램을 만들 수 있다.

long uniqueWords = 0;
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
                                          uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                                                             .distinct()
                                                             .count();
                                       } catch (IOException e) {
                                           //
                                       }

Files.lines로 파일의 각 행 요소를 반환하는 스트림을 얻을 수 있다. 스트림의 소스가 I/O 자원이므로 이 메서드를 try/catch 블록으로 감쌌고 메모리 누수를 막으려면 자원을 닫아야 한다. 기존에는 finally 블록에서 자원을 닫았다. Stream 인터페이스는 AutoCloseable 인터페이스를 구현한다. 따라서 try 블록 내의 자원은 자동으로 관리된다. line에 split 메서드를 호출해서 각 행의 단어를 분리할 수 있다. 각 행의 단어를 여러 스트림으로 만드는 것이 flatMap으로 스트림을 하나로 평면화했다. 마지막으로 distinct와 count를 연결해서 스트림의 고유 단어 수를 계산한다.

함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과는 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

iterate 메서드

Stream.iterate(0,  n -> n + 2)
      .limit(10)
      .forEach(System.out::println);

iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다. iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림을 만든다. 이러한 스트림을 언바운드 스트림이라고 표현한다.

generate 메서드

iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않는다 generate는 Supplier<T>를 인수로 받아서 새로운 값을 생산한다.

Stream.generate(Math::random)
      .limit(5)
      .forEach(System.out::println);
728x90