본문 바로가기

개발 서적/모던 자바 인 액션

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

 

 


[목차]
chapter 1. 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가?

chapter 2. 동작 파라미터화 코드 전달하기
chapter 3. 람다 표현식(1)
chapter 3. 람다 표현식(2)
chapter 4. 스트림 소개
chapter 5. 스트림 활용



 

이 장에서는 스트림 API가 지원하는 다양한 연산을 살펴본다. 다음으로 숫자 스트림, 파일과 배열 등 다양한 소스로 스트림을 만드는 방법과, 무한 스트림 등 스트림의 특수한 경우도 살펴본다.

필터링

스트림의 요소를 선택하는 방법, 즉 프레디케이트 필터링 방법과 고유 요소만 필터링 하는 방법

프레디케이트로 필터링

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

고유 요소 필터링

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

 

스트림 슬라이싱

스트림의 요소를 선택하거나 스킵하는 방법. 프레디케이트를 이용하는 방법, 스트림의 처음 몇 개의 요소를 무시하는 방법, 특정 크기로 스트림을 줄이는 방법

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

TAKEWHILE

  • filter는 조건에 대해 다 검사하며 참인 것만 다음으로 넘어가지만, takeWhile은 조건에 대해 참이 아닐경우 바로 거기서 멈추게 된다.
  • 리스트가 정렬되어 있는 경우에 유용하게 사용될 수 있다.

DROPWHILE

  • dropWhile은 takeWhile과 정반대의 작업을 수행한다. 프레디케이트가 처음으로 거짓이 되는 지점에서 작업을 중단하고, 지금까지 발견된 요소는 버린 남은 요소를 반환한다.

스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 최대 요소 n개를 반환할 수 있다.

요소 건너뛰기

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

 

매핑

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

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

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. (기존 값을 고치는 개념보다는 새로운 버전을 만든다는 개념에 가까움) 다른 map 메서드를 체이닝하는 것도 가능하다.

스트림 평면화

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

요약하면 flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

검색과 매칭

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

anyMatch

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

allMatch

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

noneMatch

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

anyMatch, allMatch, noneMatch는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.

findAny

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

Optional?

값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. findAny는 아무 요소도 반환하지 않을 수 있다. 이렇게 null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T>를 만들었다. Optional은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공한다.

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

findFirst

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. findFirst 메서드는 이런 스트림에서 첫 번째 요소를 찾는데 사용한다.

findFirst vs findAny

병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용. (병렬 실행 시, findAny는 항상 동일한 값을 반환한다는 보장이 없음)

 

리듀싱

모든 스트림 요소를 처리해서 값으로 도출하는 질의를 리듀싱 연산이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.

요소의 합

T reduce(T identity, BinaryOperator<T> accumulator);

// reduce({초기값 0}, {두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>})
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 모든 요소의 합
int sum = numbers.stream().reduce(0, Integer::sum);

초기값을 받지 않도록 오버로드된 reduce의 경우에는 Optional 객체를 반환한다. 스트림에 아무 요소도 없는 상황이라면 초기값이 없으므로 reduce는 값을 반환할 수 없기 때문이다.

Optional<T> reduce(BinaryOperator<T> accumulator);

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

최댓값과 최솟값

// 최댓값
Optional<Integer> max = numbers.stream().reduce(Integer::max);
// 최솟값
Optional<Integer> min = numbers.stream().reduce(Integer::min);

reduce 메서드의 장점과 병렬화

기존의 단계적 반복으로 합계를 구하는 것과 reduce를 이용해서 구하는 것은 어떤 차이가 있는가?

  • 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다. 강제적으로 동기화시키더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다. (이를 가변 누적자 패턴이라고 한다.)
  • reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다. (포크/조인 프레임워크를 이용하는데 7장에서 깊게 살펴본다.)
  • stream()을 parallelStream()으로 바꿔 스트림 작업을 병렬로 수행할 수 있다. 단, 람다의 상태가 바뀌지 말아야 하며, 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않아야 한다.

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

  • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다. 따라서 사용자가 제공한 람다나 메서드 참조가 내부적인 가변 상태를 갖지 않는다는 가정 하에 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산이다.
  • reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. sorted나 distinct 같은 연산은 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다. 이러한 연산을 내부 상태를 갖는 연산이라고 한다.
  • 스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정(bound)되어 있다. sorted나 dintinct 같은 연산은 어떤 요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다. (모든 소수를 포함하는 스트림을 역순 정렬할 건데 무한이라면 → 첫 번째 요소로 가장 큰 소수가 와야하는데 값이 무한대다.)

 

실전 연습

				// 2011년에 일어난 모든 트랜잭션을 찾아 값을 오름차순으로 정리하시오.
        List<Transaction> transactionsOf2011 =
                transactions.stream()
                            .filter(transaction -> transaction.getYear() == 2011)
                            .sorted(Comparator.comparing(Transaction::getValue))
                            .collect(toList());

        // 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.
        List<String> cities =
                transactions.stream()
                            .map(Transaction::getTrader)
                            .map(Trader::getCity)
                            .distinct()     // .collect(toSet());
                            .collect(toList());

        // Cambridge에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.
        List<Trader> traders =
                transactions.stream()
                            .map(Transaction::getTrader)
                            .filter(trader -> trader.getCity().equals("Cambridge"))
                            .distinct()
                            .sorted(Comparator.comparing(Trader::getName))
                            .collect(toList());

        // 모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.
        String traderStr =
                transactions.stream()
                            .map(Transaction::getTrader)
                            .map(Trader::getName)
                            .distinct()
                            .sorted()
                            .reduce("", (a, b) -> a + b); // .collect(joining());

        // Miilan에 거래자가 있는가?
        boolean existed =
                transactions.stream()
                            .map(Transaction::getTrader) // .anyMatch(transaction -> transaction.getTrader().getCity().equals("Milan"));
                            .map(Trader::getCity)
                            .anyMatch(city -> city.equals("Milan"));

        // Cambridge에 거주하는 거래자의 모든 트랜잭션 값을 출력하시오.
        transactions.stream()
                    .filter(transaction -> transaction.getTrader().getCity().equals("Cambridge"))
                    .map(Transaction::getValue)
                    .forEach(System.out::println);

        // 전체 트랜잭션 중 최댓값은 얼마인가?
        Optional<Integer> max = transactions.stream()
                                            .max(Comparator.comparing(Transaction::getValue))
                                            .map(Transaction::getValue);

        // 전체 트랜잭션 중 최솟값은 얼마인가?
        Optional<Integer> min = transactions.stream()
                                            .map(Transaction::getValue)
                                            .reduce(Integer::min);

 

숫자형 스트림

아래 코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.

int calories = menu.stream()
									 .map(Dish::getCalories) // Stream<Integer>
									 .reduce(0, Integer::sum);

// Integer.sum()의 내부 구현
public static int sum(int a, int b) {
        return a + b;
    }

// Integer.valueOf()의 내부 구현
public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

// Integer.intValue()의 내부 구현
public int intValue() {
        return value;
    }

// 박싱 비용
1. Dish 객체를 생성할 때 int형으로 파라미터를 추가할 때 Integer.valueOf() 실행됨
		→ valueOf()는 캐싱 범위(-128 ~ 127) 내의 값을 받은 경우는 IntegerCache.cache[] 배열에서 미리 생성된 Integer객체를 반환.
		→ 범위 밖인 경우 new Integer(i)로 새로 생성됨
2. reduce 메서드에서 Integer::sum 이 실행될 때, sum 메서드는 파라미터로 원시 타입인 int형을 받기 때문에 이때 언박싱이 발생. 언박싱 때는 Integer.intValue()가 실행됨. Integer객체의 final 값인 value를 리턴함.
3. reduce의 반환 타입은 T. 즉, 참조형이 와야하므로 int형인 sum의 결과 값을 다시 Integer로 변환되어야 함. 박싱 연산이 발생.

IntegerCache는 Integer 내부에 선언된 static 클래스. Integer와 함께 java.lang 패키지에 있음. java.lang 패키지는 Bootstrap Class Loader에 의해 로딩된다. 가장 최상위의 클래스 로더에 의해 로딩되기 때문에 개발자가 작성한 코드를 실행하는 시점에서는 이미 캐싱이 되어있음.
+) java 8 기준으로 java.lang은 rt.jar에 포함되어 있으며 이는 Bootstrap Class Loader의 로딩 대상이었는데 java 9부터 rt.jar는 tools.jar와 함께 사라졌다고 한다. 이부분에 대해서는 추가적으로 공부해볼 필요가 있다.

스트림 API는 숫자 스트림을 효율적으로 처리할 수 있도록 기본 특화 스트림(primitive stream specialization)을 제공한다.

기본형 특화 스트림

자바 8에서는 박싱 비용을 피할 수 있도록 세 가지 기본형 특화 스트림을 제공한다. .

  • int 요소에 특화된 IntStream
  • double 요소에 특화된 DoubleStream
  • long 요소에 특화된 LongStream

각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. 또한 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.

→ 특화 스트림은 박싱 과정에서 일어나는 효율성과 관련 있을 뿐, 추가 기능을 제공하는 것은 아님

숫자 스트림으로 매핑

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

// mapToInt() 시그니처
IntStream mapToInt(ToIntFunction<? super T> mapper);
// IntStream의 sum() 시그니처
int sum();

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

위 코드에서 mapToInt는 Stream<Integer>가 아닌 IntStream을 반환한다. 따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산할 수 있다. (스트림이 비어있으면 sum은 기본값 0을 반환)

객체 스트림으로 복원하기

숫자 스트림을 만든 다음, 원상태인 특화되지 않은 스트림으로 복원도 가능하다. 다음 예제처럼 boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

// IntStream의 구현체인 IntPipeline의 boxed() 내부 구현
abstract class IntPipeline<E_IN> extends AbstractPipeline<E_IN, Integer, IntStream> implements IntStream {
		...
		@Override
    public final Stream<Integer> boxed() {
        return mapToObj(Integer::valueOf);
    }
		...
}

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

기본값 : OptionalInt

스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 어떻게 구별할 수 있을까? Optional을 Integer, String 등의 형식으로 파라미터화할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.

OptionalInt maxCalories = menu.stream()
                              .mapToInt(Dish::getCalories)
                              .max();

// 컴파일 에러 -> Instream의 max()의 리턴 타입은 OptionalInt
Optional<Integer> maxCalories = menu.stream()
                              .mapToInt(Dish::getCalories)
                              .max();

// 값이 없을 때 기본 최댓값을 명시적으로 설정
int max = maxCalories.orElse(1);

숫자 범위

자바 8의 IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다. 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.

  • range 메서드는 시작값과 종료값이 결과에 포함되지 않는다. → 책 오류인가? 시작값은 두 메서드 모두 포함한다.
  • rangeClosed 메서드는 시작값과 종료값이 결과에 포함된다.
IntStream evenNumbersInRangeClosed = IntStream.rangeClosed(1, 100);
IntStream evenNumbersInRange = IntStream.range(1, 100);

System.out.println(evenNumbersInRangeClosed.count()); // 100
System.out.println(evenNumbersInRange.count()); // 99

숫자 스트림 활용: 피타고라스 수

피타고라스 정리(aa + bb = c*c)가 성립하는 세 정수를 리턴하는 스트림을 만들어본다.

세 수 표현하기

  • int 배열을 이용하여 표현한다. (3, 4, 5)라고 한다면 new int[]{1, 2, 3} 으로 표현한다.

좋은 필터링 조합

  • 세 수 중에 a, b 두 수만 알고 있을 때 두 수가 피타고라스 수의 일부가 될 수 있는지는 aa + bb의 제곱근이 정수인지 확인할 수 있다.
  • filter를 이용해 다음과 같이 작성할 수 있다.
**filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)**

집합 생성

  • 필터를 이용해서 a, b를 선택할 수 있게 되었다면, 마지막 세 번째 수를 찾아야 한다.
  • map을 이용해서 각 요소를 피타고라스 수로 변환할 수 있다.
stream.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
      **.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});**

b값 생성

  • Stream.rangeClosed로 주어진 범위의 수를 만들 수 있음을 배웠다.
  • 이를 이용하여 b값을 생성한다.
	**IntStream.rangeClosed(1, 100) // IntStream**
					 .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
           .**mapToObj**(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)})
  • IntStream의 map()은 스트림의 각 요소로 int가 반환될 것을 기대하지만, 우리가 원하는 것은 피타고라스의 세 수가 담기는 int[]다. → mapToObj()를 이용한다.

a값 생성

  • b와 비슷한 방법으로 a값을 생성할 수 있다.
Stream<int[]> pythagoreanTriples =
                **IntStream.rangeClosed(1, 100).boxed()**
                         **.flatMap(a ->**
                                 IntStream.rangeClosed(a, 100)
                                          .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
                                          .mapToObj(b ->
                                                  new int[]{a, b, (int) Math.sqrt(a * a + b * b)})
                         );
  • flatMap()는 생성된 각각의 스트림을 하나의 평준화된 스트림으로 만들어준다. (flatMap이 아닌 map을 사용했다면 Stream<int[]>가 아니라 Stream<Stream<int[]>>가 나왔을 것임)

개선할 점

  • 제곱근을 두 번 계산하는 로직을 개선한다.
  • (aa, bb, aa + bb) 형식을 만족하는 세 수를 만든 다음 원하는 조건에 맞는 결과만 필터링하도록 최적화한다.
Stream<double[]> pythagoreanTriples =
                IntStream.rangeClosed(1, 100).boxed()
                         .flatMap(a -> IntStream.rangeClosed(a, 100)
                                          .mapToObj(b ->
                                                  new double[]{a, b, Math.sqrt(a * a + b * b)}) // 만들어진 세 수
                                 .filter(t -> t[2] % 1 == 0)); // 세 수의 세 번째 요소는 반드시 정수여야 한다.

→ 개선하기 전 코드에서는 filter 연산이 먼저 있어서 조건에 맞지 않는 요소를 스킵할 수 있기 때문에 더 빠른거 아닌가 생각이 들었음. 숫자 범위를 1~20,000로 잡고 두 코드의 시간을 계산해봤는데 개선전 코드가 1초정도 더 빠르게 처리되었다. 책에서 말한 개선은 코드의 중복을 제거했음을 의미한건가?

스트림 만들기

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

값으로 스트림 만들기

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

/**
     * Returns a sequential {@code Stream} containing a single element.
     *
     * @param t the single element
     * @param <T> the type of stream elements
     * @return a singleton sequential stream
     */
    public static<T> Stream<T> of(T t) {
        return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
    }
// 스트림의 모든 문자열을 대문자로 변환 후 출력
Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase)
			.forEach(System.out::println);

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

/**
     * Returns an empty sequential {@code Stream}.
     *
     * @param <T> the type of stream elements
     * @return an empty sequential stream
     */
    public static<T> Stream<T> empty() {
        return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
    }
Stream<String> emptyStream = Stream.empty();

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

자바9에서는 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메소드가 추가되었다. → Stream.ofNullable()

/**
     * Returns a sequential {@code Stream} containing a single element, if
     * non-null, otherwise returns an empty {@code Stream}.
     *
     * @param t the single element
     * @param <T> the type of stream elements
     * @return a stream with a single element if the specified element
     *         is non-null, otherwise an empty stream
     * @since 9
     */
    public static<T> Stream<T> ofNullable(T t) {
        return t == null ? Stream.empty()
                         : StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
    }
// 자바9 이전. null을 명시적으로 확인했어야 했음
String homeValue = System.getProperty("home");
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(homeValue);
        
// 자바9 이후
Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

flapMap과 함께 사용하는 상황에서는 아래와 같은 패턴으로 유용하게 사용 가능하다.

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

배열로 스트림 만들기

배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들 수 있다. 아래는 IntStream을 리턴하는 메서드지만 long, double 등, 특화 시트림에 대한 오버로딩이 되어 있다.

/**
     * Returns a sequential {@link IntStream} with the specified array as its
     * source.
     *
     * @param array the array, assumed to be unmodified during use
     * @return an {@code IntStream} for the array
     * @since 1.8
     */
    public static IntStream stream(int[] array) {
        return stream(array, 0, array.length);
    }
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

파일로 스트림 만들기

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있도록 업데이트 되었다.

long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Path.get("data.txt"), Charset.defaultCharset())) {
		uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
									     .distinct()
                       .count();
}
catch (IOException e) {
}
  • Stream 인터페이스는 자원을 자동으로 해제할 수 있는 AutoCloseable 인터페이스를 구현하므로 finally에서 자원을 닫을 필요가 없다.

함수로 무한 스트림 만들기

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

iterate 메서드

iterate 메서드는 초깃값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다. 기존 결과에 의존해서 순차적으로 연산을 수행한다. 이러한 스트림을 언바운드 스트림(unbounded stream)이라고 한다.

public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
        Objects.requireNonNull(f);
        final Iterator<T> iterator = new Iterator<T>() {
            @SuppressWarnings("unchecked")
            T t = (T) Streams.NONE;

            @Override
            public boolean hasNext() {
                return true;
            }

            @Override
            public T next() {
                return t = (t == Streams.NONE) ? seed : f.apply(t);
            }
        };
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                iterator,
                Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
    }

아래 예제에서 iterate 메서드는 짝수 스트림을 생성한다. 끝이 없이 값을 생성하는 것을 limt 메서드를 이용해 크기를 10개로 제한했다.

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

피보나치수열 집합과 같은 연산도 iterate로 작성 가능하다.

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
      .limit(20)
      .forEach(t -> System.out.println("(" + t[0] + ",  " + t[1] + ")"));

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

public static IntStream iterate(int seed, IntPredicate hasNext, IntUnaryOperator next) {
	...
}

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

generate 메서드

generate는 Supplier<T>를 인수로 받아서 새로운 값을 생산한다.

public static<T> Stream<T> generate(Supplier<T> s) {
        Objects.requireNonNull(s);
        return StreamSupport.stream(
                new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
    }

0에서 1 사이에서 임의의 더블 숫자 다섯 개를 만드는 코드를 다음과 같이 작성 가능하다.

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

generate 메서드를 이용해서도 피보나치수열 집합 연산 코드를 작성할 수 있다.

IntSupplier fib = new IntSupplier() {
		private int previous = 0;
    private int current = 1;

    @Override
    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 인스턴스는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 피보나치 요소가 들어있는지 추적하므로 가변(mutable) 상태 객체다. iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서도 기존 상태를 바꾸지 않는 순수한 불변(immutable) 상태를 유지했다. → 예제를 위해 가변 객체가 되는 코드를 작성한 것이지, 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야 한다.
  • 무한 스트림의 경우 무한한 크기를 가지기 때문에 limit를 이용해서 명시적으로 스트림의 크기를 제한해야 한다. 마찬가지로 무한 스트림의 요소는 무한적이므로 계산이 반복되므로 정렬하거나 리듀스할 수 없다.
반응형