[자바 인 액션] 05 - 스트림 활용

14 minute read

Chapter 05. 스트림 활용

5.1 필터링

  • filter() : predicate를 인수로 받아서 predicate와 일치하는 모든 요소를 포함하는 스트림을 반환
  • distinct() : 고유 요소로 이루어진 스트림을 반환
// 프레디케이트로 거름
System.out.println("Filtering with a predicate");
List<Dish> vegetarianMenu = menu.stream()
  .filter(Dish::isVegetarian)
  .collect(toList());
vegetarianMenu.forEach(System.out::println);

// 고유 요소로 거름
System.out.println("Filtering unique elements:");
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
  .filter(i -> i % 2 == 0)
  .distinct()
  .forEach(System.out::println);

5.2 스트림 슬라이싱

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

스트림의 요소를 효과적으로 선택하기 위해 java 9 에 추가된 메서드

  • takeWhile() : 리스트가 이미 정렬된 상태에서 특정 값을 기준으로 필터링한다면 기준값을 넘어가는 순간 더 이상 비교(반복 작업)를 중단하도록 한다.
    • 무한 스트림 포함 모든 스트림에 적용 가능
  • dropWhile() : takeWhile()과 정반대 작업을 수행. 프레이케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환
    • 무한 스트림에도 적용 가능

스트림 축소

  • limit() : 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환

요소 건너뛰기

  • skip() : 처음 n개 요소를 제외한 스트림을 반환
// 300칼로리 이상의 처음 두 요리를 건너띈 다음에 300칼로리가 넘는 나머지 요리를 반환
List<Dish> dishes = menu.stream()
//  .filter(dish -> dish.getCalories() > 300) //모든 요소를 비교
  .takeWhile(dish -> dish.getCalories() > 300) //기준을 넘어가면 비교 중단
//  .dropWhile(dish -> dish.getCalories() <= 300) //takeWhile 결과와 동일
  .skip(2)
  .collect(toList());

5.3 매핑

특정 객체에서 특정 데이터(필드)를 선택

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

  • map() : 함수를 인수로 받는다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
// 각 Dish의 요리명 길이
List<Integer> dishNameLengths = menu.stream()
    .map(Dish::getName) //-> 출력 스트림 : Stream<String>
    .map(String::length)
    .collect(toList());

스트림 평면화

예제 ) 리스트의 각 단어의 고유 문자로 이루어진 리스트를 반환

["Hello", "World"] → ["H", "e", "l", "o", "w", "r", "d"]

words.stream()
    .map(word -> word.split("")) //-> 출력 스트림 : Stream<String[]>
    .distinct()
    .collect(toList());

위 코드에서 map으로 전달한 람다는 각 단어의 String[]을 반환한다는 점이 문제다. 따라서 map 메서드가 반환한 스트림의 형식은 Stream<String[]> 이다.

[그림 5-5] map을 이용해서 단어 리스트에서 고유 문자를 찾는 데 실패한 사례

image

map과 Arrays.stream 활용

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

반환되는 것은 스트림 리스트 (List<Stream<String>>)가 만들어져서 실패이다. 원하는 모양은 List<String>이 되어야 한다.

flatMap 사용

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

즉 map(Arrays::stream)과 달리 하나의 평면화된 스트림을 반환한다.

⇒ 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

List<String> uniqueCharacters = words.stream()
    .map(w -> w.split("")) //각 단어를 개별 문자열을 포함하는 배열로 반환
    .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
    .distinct()
    .collect(Collectors.toList());

[그림 5-6] flatmap을 이용해서 단어 리스트에서 고유 문자 찾기

image

  • 예제 ) 두 개의 숫자 리스트가 있을 때 모든 숫자 쌍의 리스트를 반환

    [1, 2, 3], [3, 4] → [(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]

    List<Integer> numbers1 = Arrays.asList(1, 2, 3);
    List<Integer> numbers2 = Arrays.asList(3, 4);
    List<int[]> pairs = numbers1.stream()
      	.flatMap(i -> numbers2.stream()
                						.map(j -> new int[]{i, j}))
      	.collect(toList());
    

    Stream<Stream<Integer[]>> 가 아닌 Stream<Integer[]>로 평면화한 스트림이 필요하기 때문에 flatMap을 사용해야 한다.

  • 예제2) 앞선 예제에서 합이 3으로 나누어 떨어지는 쌍만 반환

    List<Integer> numbers1 = Arrays.asList(1, 2, 3);
    List<Integer> numbers2 = Arrays.asList(3, 4);
    List<int[]> pairs = numbers1.stream()
      	.flatMap(i -> numbers2.stream()
                 						.filter(j -> (i + j) % 3 == 0)
                						.map(j -> new int[]{i, j}))
      	.collect(toList());
    

5.4 검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색

  • anyMatch() : Predicate가 스트림에서 적어도 한 요소와 일치하는지 확인
// menu에 채식요리가 있는지 확인
if(menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
  • allMatch() : 스트림의 모든 요소가 주어진 Predicate와 일치하는지 검사
// 모든 요리가 1000칼로리 이하면 건강식으로 간주
boolean isHealthy = menu.stream().allmatch(d -> d.getCalories() < 1000);
  • noneMatch() : 주어진 Predicate와 일치하는 요소가 없는지 확인
    • allMatch와 반대 연산을 수행
// 모든 요리가 1000칼로리 이하면 건강식으로 간주
boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

앞선 세 메서드는 스트림 쇼트서킷 기법을 활용한다.

※ 쇼트서킷

표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 된다. 이러한 상황을 쇼트서킷이라고 부른다. allMatch, noneMatch, findFirst, findAny 등의 연산은 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있다. 또는 원하는 요소를 찾았으면 즉시 결과를 반환할 수 있다. 특히 무한한 요소를 가진 스트림을 유한한 크기로 줄일 수 있는 유용한 연산이다.

  • findAny() : 현재 스트림에서 임의의 요소를 반환
Optional<Dish> dish = menu.stream()
    .filter(Dish::isVegetarian)
    .findAny();
  • findFirst() : 논리적인 아이템 순서가 정해져 있는 스트림에서 첫번째 요소를 찾아서 반환
//숫자리스트에서 3으로 나누어 떨어지는 첫 번째 제곱값을 반환
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = 
  someNumbes.stream()
  	.filter(n -> n * n)
  	.filter(n -> n % 3 == 0)
  	.findFirst(); //9

findFirstfindAny는 언제 사용하나??

사용의 차이 유무는 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

※ Optional이란?

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

Optional은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할 것인지 강제하는 기능을 제공한다.

  • isPresent() : Optional이 값을 포함하면 true를 반환하고 값을 포함하지 않으면 false를 반환
  • ifPresent(Consumer block) : 값이 있으면 주어진 블록을 실행한다. Consumer 함수형 인터페이스에는 T 형식의 인수를 받으며 void를 반환하는 람다를 전달할 수 있다.
  • T get() : 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
  • T orElse(T other) : 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.
menu.stream()
  .filter(Dish::isVegetarian)
  .findAny() // Optional<Dish> 반환
  .ifPresent(d -> System.out.println(d.getName()); // 값이 있으면 출력되고, 값이 없으면 아무 일도 일어나지 않는다.

5.5 리듀싱

리듀싱 연산이란 모든 스트림 요소를 처리해서 값으로 도출하는 연산이다.

  • 스트림 요소를 조합해서 더 복잡한 질의를 표현

    • “메뉴의 모든 칼로리의 합계는?”
    • “메뉴에서 칼로리가 가장 높은 요리는?”
  • 최종 하나의 요소가 남을 때까지 스트림의 모든 요소를 반복적으로 처리

    ⇒ 애플리케이션의 반복된 패턴을 추상화할 수 있다.

요소의 합

numbers의 각 요소의 합계를 구하려고 할때, 외부 반복을 사용하면 어떻게 구현해야 할까?

int sum = 0;
for (int x : numbers) {
    sum += x;
}
  • numbers의 각 요소는 결과에 반복적으로 더해진다.
  • 리스트에서 하나의 숫자가 남을 때 까지 reduce 과정을 반복한다.
  • 사용된 파라미터
    • sum 변수의 초기값 0
    • 리스트의 모든 요소를 조합하는 연산(+)

위의 코드를 reduce를 이용해서 다음처럼 스트림의 모든 요소를 더할 수 있다.

  • 필요한 인수
    • 초기값
    • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>
      • ex) 람다 표현식 : (a, b) → a + b , 메서드 참조 : Integer::sum
int sum = numbers.stream().reduce(0, (a, b) -> a + b); //총합
int sum = numbers.stream().reduce(0, Integer::sum); //총합
int product = numbers.stream().reduce(1, (a, b) -> a * b); //총곱

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

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

[그림 5-7] reduce를 이용해서 스트림의 모든 숫자 더하기

image

최댓값, 최솟값

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

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

퀴즈 : 리듀스

map과 reduce 메서드를 이용해서 스트림의 요리 개수를 계산하시오.

  • int count = menu.stream()
                    .map(d -> 1)
                    .reduce(0, Integer::sum); // 또는 (a, b) -> a + b
    
    • 스트림에서 각 요소를 1로 매핑한 다음에 reduce로 이들의 합계를 계산.
      • 스트림에 저장된 숫자를 차례로 더한다.

※ 맵 리듀스 패턴

  • map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라 한다.

    • 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다.
  • 4장에서 살펴본 count() 메서드로 동일한 결과를 얻을 수 있다.

    long count = menu.stream().count();
    

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

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

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

    • 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다.
    • 강제적으로 동기화시킨다 하더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다.
  • 병렬화를 위해서는 입력을 분할 하고, 분할된 입력을 더한 다음에, 더한 값을 합쳐야 한다.

    • 7장에서 포크/조인 프레임워크를 이용하는 방법을 살펴본다.
  • 스트림의 모든 요소를 더하는 코드를 병렬로 만드는 방법

    • stream()을 parallelStream()으로 바꾼다.

      int sum = numbers.parallelStream().reduce(0, Integer::sum);
      
      • 7장에서 살펴봄.
    • 위 코드를 병렬로 실행하기 위한 조건

      1. reduce에 넘겨준 람다의 상태(인스턴스 변수 같은)가 바뀌지 말아야 한다.
      2. 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구조여야 한다.

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

  • 스트림 연산은 마치 만병통치약 같은 존재다.

    • 원하는 모든 연산을 쉽게 구현할 수 있다.
    • 컬렉션으로 스트림을 만드는 stream 메서드를 parallelStream로 바꾸는 것만으로도 병렬성을 얻을 수 있다.
  • 다양하게 응용 가능

    • 요리 리스트를 스트림으로 변환 할 수 있다.
    • filter로 원하는 종류의 요리만 선택할 수 있다.
    • map을 이용해서 칼로리를 추가한 다음에 reduce로 요리의 칼로리 총합을 계산한다.
    • 병렬로 실행할 수 있다.
      • 각각의 연산은 다양한 연산을 수행하기 때문에 내부적인 상태를 고려해야 한다.
  • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.

    • 사용자가 제공한 람다나 메서드 레퍼런스가 내부적인 가변 상태를 갖지 않는 다는 가정 하에 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산이다.
  • reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다.

    • 스트림에서 처리하는 요소 수와 관계 없이 내부 상태의 크기는 한정(bounded)되어 있다.
      • int나 double과 같은 내부 상태 사용
  • sorted나 distinct 같은 연산은 filter나 map과는 달리 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다.

    • 어떤 요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다.

    • 연산을 수행하는 데 필요한 저장소 크기는 정해져있지 않다.

      • 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다.

        ex) 모든 소수를 포함하는 스트림을 역순으로 만든다. → 첫 번째 요소로 가장 큰 소수, 즉 세상에 존재하지 않는 수를 반환해야 한다.

    • 이러한 연산은 내부 상태를 갖는 연산(stateful operation)이라 한다.

5.6 실전 연습

5.5.1 거래자와 트랜잭션

Trader raoul = new Trader("Raoul", "Cambridge");
Trader mario = new Trader("Mario", "Milan");
Trader alan = new Trader("Alan", "Cambridge");
Trader brian = new Trader("Brian", "Cambridge");

List<Transaction> transaction = Arrays.asList(
        new Transaction(brian, 2011, 300),
        new Transaction(raoul, 2012, 1000),
        new Transaction(raoul, 2011, 400),
        new Transaction(mario, 2012, 710),
        new Transaction(mario, 2012, 700),
        new Transaction(alan, 2012, 950)
);
  • Trader.java

    public class Trader {
        private final String name;
        private final String city;
      
        public Trader(String name, String city) {
            this.name = name;
            this.city = city;
        }
      
        public String getName() {
            return name;
        }
      
        public String getCity() {
            return city;
        }
      
        @Override
        public String toString() {
            return "Trader{" +
                    "name='" + name + '\'' +
                    ", city='" + city + '\'' +
                    '}';
        }
    }
    
  • Transaction.java

    public class Transaction{
        private final Trader trader;
        private int year;
        private int value;
      
        public Transaction(Trader trader, int year, int value) {
            this.trader = trader;
            this.year = year;
            this.value = value;
        }
      
        public Trader getTrader() {
            return trader;
        }
      
        public int getYear() {
            return year;
        }
      
        public int getValue() {
            return value;
        }
      
        @Override
        public String toString() {
            return "Transaction{" +
                    "trader=" + trader +
                    ", year=" + year +
                    ", value=" + value +
                    '}';
        }
    }
    

5.6.2 실전 연습 문제 & 정답

  1. 2011년에 일어난 모든 트랜잭션을 찾아서 값을 오름차순으로 정렬하시오.

    List<Transaction> tr2011 = transactions.stream()
              .map(transaction -> transaction.getYear() == 2011)
              .sorted(comparing(Transaction::getValue))
              .collect(toList());
    
  2. 거래자가 근무하는 모든 도시를 중복 없이 나열하시오.

    List<String> cities = transactions.stream()
              .map(transaction -> transaction.getTrader().getCity())
              .distinct()
              .collect(toList());
    
    • distinct() 대신에 스트림을 집합으로 변환하는 toSet()을 사용할 수 있다.

      List<String> cities = transactions.stream()
              .map(transaction -> transaction.getTrader().getCity())
              .collect(toSet());
      
  3. 케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오. //거래자정보가 담겨있어야 한다.

    List<Trader> cambridgeTrader = transactions.stream()
                    .map(transaction -> transaction.getTrader())
                    .filter(trader -> trader.getCity().equals("Cambridge"))
                    .distinct()
                    .sorted(Comparator.comparing(Trader::getName))
                    .collect(Collectors.toList());
    cambridgeTrader.forEach(System.out::println);
    
  4. 알파벳 순으로 정렬된 모든 거래자의 이름 “문자열”을 반환

    String traderStr = transactions.stream()
            .map(transaction -> transaction.getTrader().getName())
            .distinct()
            .sorted()
            .reduce("", (n1, n2) -> n1 + n2);
    
    • 각 반복과정에서 모든 문자열을 반복적으로 연결해서 새로운 문자열 객체를 만든다.

    • 위 코드는 효율성이 부족하다.

    • joining()을 이용하는 것이 더 효율적이다. (joining은 내부적으로 StringBuilder를 이용한다.)

      List<String> traderStr =   transactions.stream()
              .map(transaction -> transaction.getTrader().getName())
              .distinct()
              .sorted()
              .collect(joining());
      
  5. 밀라노에 거래자가 있는가?

    boolean milanBased = transactions.stream()
                .anyMatch(transaction -> transaction.getTrader()
                                                .getCity()
                                                .equals("Milan"));
    
  6. 케임브리지에 거주하는 거래자의 모든 트랜잭션값을 출력하시오.

    transactions.stream()
            .filter(t -> "Cambridge".equals(t.getTrader().getCity()))
            .map(Transaction::getValue)
            .forEach(System.out::println);
    
  7. 전체 트랜잭션 중 최댓값은 얼마인가?

    int highestValue = transactions.stream()
            .map(Transaction::getValue)
            .reduce(0, Integer::max); //.max(Integer::compareTo);
    
  8. 가장 작은 값을 가진 트랜잭션 탐색 //트랜잭션을 찾아야한다.

    Optional<Transaction> minTransaction = transactions.stream()
            .reduce((t1, t2) ->
                    t1.getValue() <= t2.getValue() ? t1 : t2);
    
    • 스트림은 최댓값과 최솟값을 계산하는데 사용할 키를 지정하는 Comparator를 인수로 받는 min과 max 메서드를 제공한다.

      Optional<Integer> smallestTransaction = 
        transactions.stream()
                    .min(comparing(Transaction::getValue));
      
  9. 거래가 없을 때 기본 문자열을 사용할 수 있도록 발견된 거래가 있으면 문자열로 바꾸는 꼼수를 사용함(예, the Stream is empty)

    System.out.println(smallestTransaction.map(String::valueOf).orElse("No transactions found"));
    

5.7 숫자형 스트림

이전의 소스코드에서 reduce 메서드로 스트림 요소의 합을 구하는 예제를 살펴봤다.

menu.stream()
    .map(Dish::getCalories)
    .reduce(0, Integer::sum); //박싱 비용

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

map 메서드는 Stream<T> 를 생성한다. 앞서 코드의 스트림의 요소 형식은 Integer이지만 인터페이스에는 sum 메서드가 없기 때문에 map() 이후 sum()을 바로 호출할 수 없다. 제네릭 타입 T에는 sum 연산을 수행할 수 없는 Dish와 같은 데이터 타입이 올 수 있기 때문이다.

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

5.7.1 기본형 특화 스트림

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

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

각각의 인터페이스는 sum, max와 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.

또한, 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.

스트림은 오직 박싱 과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하지는 않는다.

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다.

map과 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환한다.

menu.stream() //Stream<Dish> 반환
    .mapToInt(Dish::getCalories) // IntStream 반환. Stream<Integer>가 아님!!
    .sum();

mapToInt 메서드는 각 요리에서 모든 칼로리(Integer 형식)을 추출한 다음에 IntStream을 반환한다. 따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산할 수 있다. (스트림이 비어있으면 sum은 기본값 0을 반환한다.)

객체 스트림으로 복원하기

boxed()를 이용해 숫자스트림(IntStream)을 만든 후, 원상태인 특화되지 않은 스트림(Stream<Integer>)으로 복원할 수 있다.

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

기본값: OptionalInt

합계 예제에서는 0이라는 기본값이 있었으므로 별 문제가 없었다. 하지만 IntStream에서 최댓값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다. 그럴 경우 Optional을 사용하여 최댓값이 없는 상황에 사용할 기본값을 명시적으로 정의할 수 있다.

  • Optional을 Integer, String, 등의 참조 형식으로 파라미터화할 수 있다.
  • OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공
// OptionalInt를 이용해서 intStream의 최댓값 요소를 찾을 수 있다.
OptionalInt maxCalories = menu.stream()
    .mapToInt(Dish::getCalories)
    .max(); //반환 타입이 OptionalInt

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

5.7.2 숫자 범위

특정 범위의 숫자를 이용

자바 8의 IntStream과 LongStream은 다음 메서드 제공

  • range(시작, 끝) : 끝을 포함하지 않음
  • rangedClosed(시작, 끝) : 끝을 포함

rangedClosed를 이용해서 1부터 100까지의 숫자를 만들 수도 있다.

IntStream evenNumbers = IntStream.rangeClosed(1, 100) // [1,100] 범위
    .filter(x -> x % 2 == 0); // 1부터 100까지의 짝수 스트림 //filter를 호출해도 실제로는 아무 계산이 이루어지지 않는다.
SYstem.out.println(evenNumbers.count()) //50 //최종연산인 count() 호출했을 때 스트림을 처리한다.

5.8 스트림 만들기

5.8.1 값으로 스트림 만들기

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

Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action") // 문자열 스트림 만들기
stream.map(String::toUpperCase).forEach(System.out::println); //문자열을 대문자로 변환 후 출력

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

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

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

때로는 null이 될 수 있는 객체를 스트림(객체가 null이라면 빈 스트림)으로 만들어야 할 수있다.

예를 들어, System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다. 이런 메서드를 스트림에 활용하려면 다음 처럼 null을 명시적으로 확인해야 했다.

String homeValue = System.getProperty("home");
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(value);

Stream.ofNullable을 이용해서 다음처럼 코드를 구현할 수 있다.

Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

null이 될 수 있는 객체를 포함하는 스트림값을 flatMap과 함께 사용하는 상황에서는 이 패턴을 더 유용하게 사용할 수 있다.

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

5.8.3 배열로 스트림 만들기

배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들 수 있다.

// 기본형 int로 이루오진 배열을 IntStream으로 변환할 수 있다.
int[] numbers = {2, 3, 4, 5, 6}
int sum = Arrays.stream(numbers).sum();

5.8.4 파일로 스트림 만들기

자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있도록 업데이트되었다.

  • java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다.
  • Files.lines : 주어진 파일의 각 행 요소를 반환하는 스트림을 문자열로 반환

5.8.5 함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드 Stream.iterateStream.getnerate를 제공한다. iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다. 따라서 무제한으로 값을 계산할 수 있어서 보통 무한한 값을 출력하지 않도록 limit(n)함수를 함께 연결해서 사용한다.

Iterate

iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림을 만든다. 이러한 스트림을 언바운드 스트림(unbounded stream)이라고 표현한다. ← 스트림과 컬렉션의 가장 큰 차이점

  • 일반적으로 연속된 일련의 값을 만들 때 사용
Stream.iterate(0, n -> n + 2)
    .limit(10)
    .forEach(System.out::println); // 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20
  • 자바 9의 iterate 메서드

    두 번째 인수로 프레디케이트를 받아 언제까지 작업을 수행할 것인지의 기준으로 사용한다.

    // 0에서 시작해서 +4씩 숫자 생서. 100보다 크면 숫자 생성을 중단하는 코드
    IntStream.iterate(0, n -> n < 100, n -> n + 4);
      
    // filter는 종료 시점을 알 수 없기 때문에 잘못된 코드이다.
    IntStream.iterate(0, n -> n + 4)
      .filter(n -> n < 100);
      
    // 스트림 쇼트서킷을 지원하는 takeWhile을 이용해야 한다.
    IntStream.iterate(0, n -> n + 4)
      .takeWhile(n -> n < 100);
    

Generate

iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않는다.

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

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

요약

  • 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
  • filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
  • map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
  • findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, nonMatch, anyMatch 메서드를 이용해서 주어진 Predicate와 일치하는 요소를 스트림에서 검색할 수 있다.
  • 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
  • reduce 메서드로 스트림의 모든 요소를 반복 조함하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최댓값이나 모든 요소의 합계를 계산할 수 있다.
  • filter, map 등은 상태를 저장하지 않는 상태 없는 연산이다. reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 상태 있는 연산이라고 부른다.
  • IntStream, DoubleStream, LongStreeam은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
  • 컬렉션뿐 아니라 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
  • 크기가 정해지지 않은 스트림을 무한 스트림이라고 한다.