[Ch5] 스트림 활용

5장에서는 스트림의 여러가지 메서드를 어떻게 활용하는 지 살펴본다.

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

스트림 메서드 종류

연산 반환형식 함스 디스크립터
filter Stream T -> boolean
distinct Stream  
takeWhile Stream T -> boolean
dropWhile Stream T -> boolean
skip long  
limit Stream long
map Stream T -> R
flatMap Stream T -> Stream
sorted Stream (T, T) -> int
anyMatch boolean T -> boolean
noneMatch boolean T -> boolean
allMatch boolean T -> boolean
findAny Optional  
findFirst Optional  
forEach Consumer T -> void
collect Collector<T, A, R>  
reduce Optional (T, T) -> T
count long  
  • Operator : 반환값과 인수의 타입이 같은 함수
  • Predicate : boolean 반환
  • Function : 인수와 반환타입이 다른 함수
  • Supplier : 인수를 받지 않고 값을 반환
  • Consumer : 인수를 하나 받고 반환값은 없는 함수

reduce 메서드의 장점과 병렬화

mapreduce 메서드를 연결하는 방법을 맵 리듀스 패턴이라고 한다. 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에서 적용하면서 유명해졌다.

스트림의 각 요소를 1로 매핑한 뒤, reduce 로 이들의 합계를 계산하는 방식으로 count 의 효과를 낼 수 있다.

int count = menu.stream().map(d -> 1).reduce(0, (a,b) -> a + b);

그렇다면, 기존의 단계적 반복으로 합계(혹은 카운팅)을 구하는 것과 reduce를 이용한 것은 무슨 차이가 있을까?

reduce를 이용하면 내부반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다. 반면, 기존의 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다. (가변 누적자 패턴)

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

스트림 연산 : 상태 없음과 상태 있음

내부 상태를 갖지 않는 연산은 map, filter 와 같이 참조하는 값이 없고 출력 스트림으로 보내는 연산이다. 반면, reudce, sum, max 와 같은 연산은 결과를 누적할 내부 상태가 필요하다. 따라서 이러한 스트림은 상태를 갖는 스트림이며, 내부 상태의 크기가 한정 되어 있다.

기본형 특화 스트림

int calories = menu.stream().map(Dish:getCaloreis).reduce(0, Integer::sum);

위와 같이 reduce 메서드로 스트림 요소의 합을 구할 수 있다. 하지만 위 코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Intger를 기본형으로 언박싱해야 한다.

이러한 상황에서 쓸 수 있는 것이 기본형 특화 스트림이다. 숫자 스트림을 효율적으로 처리할 수 있게 해준다.

숫자 스트림으로 매핑

mapToInt, mapToDouble, mapToLong 세 가지 메서드를 사용해서 특화된 스트림을 반환할 수 있다. IntStream, DoubleStream, LongStream

int calories = menu.stream().mapToInt(Dish:getCaloreis).sum();

이와 비슷한 맥락으로 Optional 또한 각 기본형에 대한 특화된 Optional을 지원한다. OptionalInt, OptionalDouble, OptionalLong

객체 스트림으로 복원하기

특화된 스트림을 만든 다음에, 원상태인 스트림으로 복원할 수 있을까? boxed 메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed(); // 숫자 스트림을 스트림으로 반환

숫자 범위

IntStream 과 LongStream 에는 rangerangeClosed 두 가지 정적 메서드를 제공한다.

  • range : 시작값과 종료값이 결과에 포함되지 않는다.
  • rangeClosed : 시작값과 종료값이 결과에 포함된다.

스트림 만들기

stream 메서드로 컬렉션에서 스트림을 얻을 수 있었으며, 범위의 숫자에서 스트림을 만들 수도 있다. 일련의 값, 배열, 파일, 심지어 함수를 이용해서 무한 스트림을 만드는 방법을 설명한다.

값으로 스트림 만들기

Stream.of 로 임의의 값을 인수로 받아서 스트림을 생성할 수 있다.

Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");

stream.map(String::toUpperCase).forEach(System.out::println);

Stream<String> emptyStream = Stream.empty(); // 스트림을 비울 수도 있다.

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

Stream.ofNullable 로 Null이 될 수 있는 객체를 간단히 스트림으로 만들 수 있다. (if-else 로 null 체크를 하지 않아도)

배열로 스트림 만들기

Arrays.stream(배열)로 간단히 스트림을 만들 수 있다.

Arrays.stream(int[] numbers).sum();

파일로 스트림 만들기

NIO API (java.nio.file.Files) 의 많은 정적 메서드가 스트림을 반환한다. 예를 들어, File.lines 는 주어진 파일의 행 스트림을 문자열로 반환한다.

함수로 무한 스트림 만들기

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

iterate 와 generate 에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다. 따라서 무제한으로 값을 계산할 수 있다

iterate

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

iterate 메서드는 람다함수로 끊임 없이 짝수 스트림을 만들어 낸다. 이러한 스트림을 언바운드 스트림 이라고 한다. 이런 특징이 스트림과 컬렉션의 가장 큰 차이점이다.

일반적으로 연속된 일련의 값을 만들 때는 iterate를 사용한다.

자바9의 iterate 메서드는 프리디게이트를 지원한다.

IntStream.iterate(0, n -> n<100, n -> n+4)
         .forEach(System.out::println);

위의 코드는 두번째 인수로 Predicate를 인수로 받아 언제까지 작업을 수행할 것인지의 기준으로 사용한다.

generate

iterate 와 달리 generate 는 생산된 각 값을 연속적으로 계산하지 않는다. Supplier 를 인수로 받아서 새로운 값을 생산한다.

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

단순하게 Math.random() 메서드를 수행 (발행자) 해서 얻은 값 5개를 프린트한다. 이 발행자는 상태를 가지고 있지 않는다. 병렬 코드에는 발행자에 상태가 있으면 안전하지 않다.

IntStream 에도 generate 메서드가 있다.

IntStream ones = IntStream.generate(() -> 1);

위 코드를 IntSupplier 객체를 익명 클래스로 구현해보자

IntStream twos = IntStream.generate(new IntSupplier() {
  public int getAsInt() {
    return 2;
  }
})

여기서 쓰인 익명 클래스는 람다와 비슷한 연산을 수행하지만 익명 클래스에서는 getAsInt 메서드의 연산을 커스터마이즈 할 수 있는 상태 필드를 저으이할 수 있다는 점에서 다르다. 다시 말해, 익명클래스 안에서 필드를 선언해서 연산을 가질 수 있으므로 부작용이 생길 수 있다.

IntSupplier fib = new IntSupplier() {
  private int previous = 0;
  private int current = 1;
  public int getAsInt() {
    int oldPrevious = this.previous;
    int nextValue = this.previous + this.current;
    this.previous = this.current;
    this.current = nextValue;
    return oldPrevious;
  }
}

IntStream.generate(fib).limit(10).forEach(System.out::println);

IntSupplier 객체는 previous, current 의 값을 갱싱하므로 가변 상태 객체다. getAsInt 를 호출하면 객체 상태가 바뀌며 새로운 값을 생산한다. 스트림을 병렬로 처리하면서 올바를 결과를 얻으려면 위와 같이 가변상태인 객체가 아닌 불변 상태 기법을 고수해야 한다.

댓글남기기