Language/Java

[모던 자바 인 액션] Chap5. 스트림 활용

뚜sh뚜sh 2024. 2. 1. 21:41

필터링

1. 프레디케이트로 필터링

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

 

2. 고유 요소 필터링

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

 

 

 

스트림 슬라이싱

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

  • TAKEWHILE
    • 데이터에서 칼로리가 300 이상인 데이터만 뽑고 싶다면 filter를 사용하면 됨, 여기서 데이터가 정렬되어 있는 상태였다면 TAKEWHILE을 사용하여 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해서 스트림을 슬라이스 할 수 있음
  • DROPWHILE
    • 정렬된 데이터에서 칼로리가 300 이상인 데이터만 뽑고 싶다면 DROPWHILE을 사용할 수 있음
    • dropWhile은 takeWhile과 정반대의 작업을 수행하며 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버림
    • 프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환함
List<Dish> slicedMenu2 = specialMenu.stream()
				.dropWhile(dish -> dish.getCalories() < 300)
				.collect(toList());

 

2. 스트림 축소

  • 스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원함
  • 스트림이 정렬되어 있으면 최대 요소 n개를 반환할 수 있음
  • 정렬되지 않은 스트림에도 limit을 사용할 수 있으나 limit의 결과도 정렬되지 않은 상태로 반환됨

 

3. 요소 건너뛰기

  • 스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원함
  • n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환됨
  • limit(n)과 skip(n)은 상호 보완적인 연산을 수행함

 

 

 

매핑

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

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

 

2. 스트림 평면화

  • 리스트에서 고유 문자로 이루어진 리스트를 반환하려면 flatMap이라는 메서드를 이용하면 해결할 수 있음
  • flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑함
  • 따라서 flatMap은 하나의 평면화된 스트림을 반환함
  • flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행함

 

 

 

검색과 매칭

  • anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법을 사용함

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

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

 

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

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

 

3. NONEMATCH

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

※ 쇼트서킷 평가

  • 예를 들어 여러 and 연산으로 연결된 커다란 불리언 표현식을 평가한다고 가정했을 때 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 됨
  • 위와 같은 상황을 쇼트서킷이라고 부름
  • allMatch, noneMatch, findFirst, findAny 등의 연산은 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있음
  • 마찬가지로 스트림의 모든 요소를 처리할 필요 없이 주어진 크기의 스트림을 생성하는 limit도 쇼트서킷 연산임

 

 

4. 요소 검색

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

 

 

 

Optional

  • Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스임
  • 자바 8 라이브러리 설계자는 null은 쉽게 에러를 일으킬 수 있어서 Optional<T>를 만들게 됨
  • isPresent() : Optional이 값을 포함하면 true을 반환하고, 값을 포함하지 않으면 거짓을 반환함
  • ifPresent(Consumer<T> block) : 값이 있으면 주어진 블록을 실행함
  • T get() : 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킴
  • T orElse(T other) : 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환함

 

 

 

5. 첫 번째 요소 찾기

  • 리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있음
  • 이런 스트림에서 첫 번째 요소를 찾으려면 findFirst()를 사용하면 됨

※ findAny와 findFirst는 언제 사용할까?

  • 병렬 실행에서는 첫 번째 요소를 찾기 어려움
  • 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용함

 

 

 

리듀싱

  • 스트림 요소를 조합해서 더 복잡한 질의를 수행하려면 스트림의 모든 요소를 반복적으로 처리해야 하는데 이런 질의를 리듀싱 연산이라고 함
  • 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이(우리의 스트림)를 작은 조각이 될 떄까지 반복해서 접는 것과 비슷하다는 의미로 폴드라고 부름

1. 요소의 합

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);
  • reduce를 이용해서 위처럼 스트림의 모든 요소를 더할 수 있음
  • reduce는 두 개의 인수를 가짐
    • 초깃값 0
    • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>

※ 초깃값 없는 reduce

  • 초깃값을 받지 않도록 오버로드된 reduce도 있음
  • 그러나 이 reduce는 Optional 객체를 반환함
  • 왜냐하면 스트림에 아무 요소가 없는 상황이면 초깃값이 없으므로 reduce는 합계를 반환할 수 없음
  • 그래서 Optional 객체로 감싼 결과를 반환함

 

2. 최댓값과 최솟값

  • 최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있음
  • reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행하면서 최댓값을 생성함
  • 최솟값 연산도 최댓값과 마찬가지임
Optional<Integer> max = numbers.stream().reduce(Integer::max);

※ reduce 메서드의 장점과 병렬화

  • 기존의 단계적 반복으로 합계를 구하는 것과 reduce를 이용해서 합계를 구하는 것의 차이는 reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 됨
  • 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어려움
  • 강제적으로 동기화시키더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다는 사실을 알게 될 것임

 

 

 

[Quiz1] 다음 중 스트림 슬라이싱과 관련된 메서드의 설명 중 옳지 않은 것은?

1. dropwhile은 takewhile과 정반대의 작업을 수행한다.

2. skip(n)은 마지막 n개 요소를 제외한 스트림을 반환한다.

3. 오름차순으로 정렬된 데이터에서 무게가 40kg 이하인 데이터를 추출해야 한다면, filter 대신 takewhile을 사용할 수 있다.

4. limit(n)은 n이하의 크기를 갖는 새로운 스트림을 반환한다.

[정답] 2번 - skip(n)은 처음 n개 요소를 제외한 스트림을 반환한다.

 

 

[Quiz2] 오름차순으로 정렬된 무게 데이터가 주어졌을 때, 무게가 40kg 이상인 데이터를 추출해야 한다면 filter 대신 어떤 메서드를 활용할 수 있을까요?

[정답] takewhile

 

 

[Quiz3] 다음 중 스트림 검색과 매칭에 관련된 메서드의 설명 중 옳지 않은 것은?

1. anyMatch, allMatch, noneMatch, limit 메서드는 스트림 쇼트서킷 연산이다.

2. allMatch는 최종 연산이다.

3. 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는 지 확인할 때 anyMatch 메서드를 이용한다. 

4. findAny는 병렬 스트림에서 효율적이지 않다.

[정답] findAny는 병렬 스트림에서 효율적이다.

 

 

 

숫자형 스트림

1. 기본형 특화 스트림

  • 스트림 API 숫자 스트림을 효율적으로 처리할 수 있음
  • 자바 8에서는 3가지 기본형 특화 스트림을 제공함
  • 스트림 API는 박싱 비용을 피할 수 있도록 'int 요소에 특화된 IntStream', 'double 요소에 특화된 DoubleStream', 'long 요소에 특화된 LongStream'을 제공함
  • 각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값요소를 검색하는 max같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공함
  • 또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공
  • 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련 있으며 추가 기능을 제공하지 않음

※ 숫자 스트림으로 매핑

  • 스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용함
  • 이들 메서드는 map과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환함
int calories = menu.stream().mapToInt(Dish::getCalories).sum();
  • mapToInt 메서드는 각 요리에서 모든 칼로리(Integer 형식)를 추출한 다음에 IntStream(Stream<Integer>가 아님)을 반환함
  • 스트림이 비어있으면 sum은 기본값 0을 반환함
  • IntStream은 max, min, average 등 다양한 유틸리티 메서드도 지원함

 

※ 객체 스트림으로 복원하기

  • boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환 가능
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

 

※ 기본값 : OptionalInt

zz