본문 바로가기
그룹 스터디 공부(IT 서적)/모던 자바 인 액션

chapter 5 스트림 활용

by hanyugyeong 2023. 7. 22.
반응형
SMALL

4장에서는 스트림을 이용해서 외부 반복을 내부 반복으로 바꾸는 방법을 살펴봤다.
데이터 컬렉션 반복을 명시적으로 관리하는 외부 반복 코드다.

List<Dish> vegetarianDishes = new ArrayList<>();
   for(Dish d:menu){
     if(d.isVegetarian()){
       vegetarianDishes.add(d);
     }
   }

명시적 반복 대신 filter와 collect 연산을 지원하는 스트림 API를 이용해서 데이터 컬렉션 반복을 내부적으로 처리할 수 있다.

List<Dish> vegetarianDishes = menu.stream()
                                      .filter(Dish::isVegetarian)
                                      .collect(Collectors.toList());

5.1 필터링

5.1.1 프레디케이트로 필터링

스트림 인터페이스는 filter 메소드를 지원하는데, 이 filter 메소드는 predicate 함수를(Boolean 타입 함수) 를 인수로 받아서 프리디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

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

5.1.2 고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 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);

5.2 스트림 슬라이싱

스트림 슬라이싱은, 스트림의 요소를 선택하거나 스킵하는 것을 말한다.

Predicate를 이용한 슬라이싱

자바 9 이상 부터는 takeWile 메소드와 dropWhile 메소드를 제공한다.

TAKEWILE

  • takeWile 메소드는, predicate를 만족하지 않는 첫 번째 지점부터 마지막 스트림을 버린다.
//전체 리스트 순회
List<Dish> filteredMenu = menu.stream()
            .filter(dish -> dish.getCalories() < 320)
            .collect(Collectors.toList());
//조건에 맞지 않으면 stop 
List<Dish> slicedMenu1 = specialMenu.stream()
            .takeWhile(dish -> dish.getCalories() < 320)
            .collect(toList());

takeWhile 메소드는 조건에 맞지않는 그 순간 Stream 조회를 종료하기 때문에 정렬되어있는 Stream에 매우 유용하다.

DROPWHILE

  • dropWhile 메소드는, takeWhile과 정반대의 결과를 추출한다.
  • 첫 번째 요소부터, predicate를 처음으로 만족하는 지점까지 버리고 나머지를 반환한다.
    List<Dish> filterMenu = specialMenu.stream()
              .dropWhile(dish -> dish.getCalories() < 320)
              .collect(toList());
    dropWhile 은 프리디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.

프리디케이트가 거짓이되면, 그 즉시 작업을 중단하고 남은 모든 요소를 반환한다.

스트림 축소

  • limit(n):주어진 값 이하의 크기를 가지는 새로운 스트림을 반환하는 스트림 메소드이다
    //2개 요소 반환
    List<Dish> dishes = specialMenu.stream()
              .filter(dish->dish.getCalories() > 300)
              .limit(2)
              .collect(toList());
    limit은 조건에 맞는 요소가 채워지면 그 즉시 스트림을 반환한다.

스트림 요소 건너띄기

  • Skip(n):스트림은 처음 n 개의 요소를 제외한(스킵한) 스트림을 반환하는 메소드를 지원한다.
  • limit(n) 과 정반대의 결과를 도출한다.
  • n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환됀다.
List<Dish> skipMenu = specialMenu.stream()
            .filter(dish->dish.getCalories() < 320)
            .skip(3)
            .collect(toList());

5.3 스트림 매핑

  • 특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다.
  • Stream 에서는 Map 과 FlatMap 메소드로 특정 데이터를 선택할 수 있는 기능을 제공한다.

Stream Map

  • 스트림은 함수를 인수로 받는 map 메소드를 지원한다.
  • 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑됀다.
  • 이 과정은 요소를 고친다는 개념보다, 새로운 버전을 만든다라는 개념에서 Trans(변환) 보다 mapping(매핑) 으로 지어졌다고한다.
    // 요리명을 추출하는 코드 
    List<String> dishNames = menu.stream()
                               .map(Dish::getName)
               .collect(toList());
    

// 단어 리스트 글자수 추출
List words = Arrays.asList("Modern","Java", "In","Action");
List wordLengths = words.stream()
.map(String::length)
.collect(toList());

// map 을 연결하여 요리명의 글자수 추출
List dishNameLength = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());

## Stream FlatMap(스트림 평면화)
* flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

예를들어, list 혹은 배열의 요소들을 각각의 스트림으로 만든 후, 이 만들어진 모든 스트림 요소를 하나의 스트림으로 만들어 어떤 작업을 진행하고 싶을때 flatMap을 사용한다.
* 예를들어 [“hello”, “world”] 리스트가 [“h”, “e”, “l”, “l”, “o”, “w”, “o”, “r”, “l”, “d”] 가 되도록 변경
```java
List<String> uniqueCharacters = words.stream()
                                    .map(word->word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 변환
                                    .flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화 
                                    .distinct()
                                    .collect(Collectors.toList());

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

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

퀴즈

  1. 숫자 리스트가 주어졌을 때 제곱근으로 이루어진 리스트를 반환하시오.
  2. List<Integer> numbers = Arrays.asList(1,2,3,4,5); List<Integer> squares = numbers.stream() .map(n->n*n) .collect(Collectors.toList()); System.out.println("squares===>"+squares);
  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());
  5. 이전 예제에서 합이 3으로 나누어떨어지는 쌍만 반환하려면 어떻게 해야 할까?
  6. 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 검색과 매칭

  • 특정 속성이 데이터 집합에 있는지 여부, 즉 검색 처리도 스트림 API에서 제공합니다.
  • allMatch, anyMatch, noneMatch, findFirst, findAny

anyMatch

  • Predicate가 적어도 한 요소와 일치 하는지 확인
    if(menu.stream().anyMatch(Dish::isVegetarian)){
        System.out.println("The menu is (somewhat) vegetarian friendly!!");
      }
    allMatch
  • Predicate가 모든 요소와 일치하는지 검사
boolean isMatch = menu.stream().allMatch(Dish -> dish.getCalories() < 1000)

NoneMatch

  • noneMatch는 allMatch와 반대 연산을 수행합니다.
  • 즉. noneMatch는 주어진 predicate와 일치하는 요소가 없는지 확인한다.
    boolean noneMatch = menu.stream().noneMatch(dish -> dish.getCalories() > 1000);

요소검색(findAny,findFirst)

findAny

  • findAny 메서드는 현재 스트림에서 임의의 요소를 반환합니다.
  • findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있습니다.
 // filter 와 findAny를 활용해 채식 요리를 선택하는 방법
    Optional<Dish> dish = menu.stream()
        .filter(Dish::isVegetarian)
        .findAny();    //findAny는 Optional 객체를 반환

Optional이란?

Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다. Optional은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공한다.

  • isPresent()는 Optional이 값을 포함하면 참(true)을 반환하고, 값을 포함하지 않으면 거짓(false)을 반환한다.
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 블록을 실행한다. Consumer 함수형 인터페이스에는 T형식의 인수를 받으며 void를 반환하는 람다를 전달할 수 있다.
  • T get()은 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
  • T orElse(T other)는 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.
      menu.stream()
          .filter(Dish::isVegetarian)
          .findAny()
          .ifPresent(dish->System.out.println(dish.getName())); //값이 있으면 출력되고, 값이 없으면 아무 일도 일어나지 않는다.
    findFirst
  • 첫 번째 요소 찾기
  • 리스트 또는 정렬된 데이터로 부터 생성된 순서가 정해진 스트림에서 첫번째 요소를 찾기 위한 방법이다.
//  3으로 나누어 떨어지는 첫 번째 제곱근 값을 반환하는 코드.
    List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
    Optional<Integer> first = someNumbers.stream()
        .map(n -> n * n)
        .filter(n -> n % 3 == 0)
        .findFirst();

5.5 리듀싱

스트림의 모든 요소를 반복적으로 처리해야 한다. 이런 질의를 리듀싱 연산(모든 스트림 요소를 처리해서 값으로 도출하는)이라고 한다.

요소의 합

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

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

int sum = numbers.stream().reduce(0,(a,b)->a+b);

reduce는 두 개의 인수를 갖는다.

  • 초깃값 0
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator,예제에서는 람다 표현식 (a,b)->a+b를 사용했다.

reduce로 다른 람다, 즉 (a,b)->a*b를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다.

int product = numbers.stream().reduce(0,(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));

최댓값과 최솟값

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

  • 초깃값
  • 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다

최대값

Optional<Integer> max = numbers.stream().reduce(Integer::max);

최솟값

Optional<Integer> min = numbers.stream().reduce(Integer::min);

퀴즈 5-3 리듀스

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

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

map과 reduce를 연결하는 기법을 맵 리듀스패턴이라 하며, 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다.

5.6 실전 연습

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

List<Transaction> tr2011 = transactions.stream()
                                .filter(transaction -> transaction.getYear() == 2011) // 2011년에 발생한 트랜잭션을 필터링하도록 프레디케이트를 넘겨줌
                                .sorted(Comparator.comparing(Transaction::getValue))
                                .collect(Collectors.toList());//결과 스트림의 모든 요소를 리스트로 반환

거래자가 근무하는 모든 도시를 중복 없이 나열하시오.

List<String> cities =
                    transactions.stream()
                                .map(transaction -> transaction.getTrader().getCity()) // 트랙잰션과 관련한 각 거래사의 도시 추출
                                .distinct() //고유 도시만 선택
                                .collect(Collectors.toList());    

아직 배우지 않았지만 distinct() 대신에 스트림을 집합으로 변환하는 toSet()을 사용할 수 있다.

케임브리지에서 근무하는 모든 거래자를 찾아서 이름순으로 정렬하시오.

List<Trader> traders = transactions.stream()
                            .map(Transaction::getTrader)   //트랜잭션의 모든 거래자 추출
                            .filter(trader1 -> trader1.getCity().equals("Cambridage")) //케임브리지의 거래자만 선택
                            .distinct() //중복이 없도록 확인
                            .sorted(Comparator.comparing(Trader::getName)) // 결과 스트림의 거래자를 이름으로 정렬
                            .collect(Collectors.toList());

모든 거래자의 이름을 알파벳순으로 정렬해서 반환하시오.

String traderStr = transactions.stream()
                                  .map(transaction -> transaction.getTrader().getName()) // 모든 거래자명을 문자열 스트림으로 추출
                                  .distinct() //각각의 이름을 하나의 문자열로 연결하여 결국 모든 이름 연결 
                                  .sorted() // 이름을 알파벳순으로 정렬
                                  .reduce("",(n1,n2)-> n1+n2);//각각의 이름을 하나의 문자열로 연결하여 결국 모든 이름 연결

각 반복 과정에서 모든 문자열을 반복적으로 연결해서 새로운 문자열 객체를 만든다. 다음 코드에서는 joining()을 이용해서 더 효율적으로 문제를 해결하는 방법을 설명한다.

String traderStr =
        transactions.stream()
                    .map(transaction -> transaction.getTrader().getName()) // 모든 거래자명을 문자열 스트림으로 추출
                    .distinct() //각각의 이름을 하나의 문자열로 연결하여 결국 모든 이름 연결
                    .sorted() // 이름을 알파벳순으로 정렬
                    .collect(Collectors.joining());

밀라노에 거래자가 있는가?

boolean milanBased =
                  transactions.stream()
                              .anyMatch(transaction -> transaction.getTrader()
                                                                  .getCity()
                                                                  .equals("Milan"));//anyMatch에 프레디케이트를 전달해서 밀라노에 거래자가 있는지 확인 

케임브리지에 거주하는 거래자의 모든 트랜잭션값을 출력하시오.

transactions.stream()
                .filter(t->"Cambridage".equals(t.getTrader().getCity()))
                .map(Transaction::getValue)
                .forEach(System.out::println);

전체 트랙잭션 중 최대값은 얼마인가?

Optional<Integer> highestValue =
        transactions.stream()
                    .map(Transaction::getValue) //각 트랜잭션의 값 추출
                    .reduce(Integer::max);  //결과 스트림의 최대값 계산

전체 트랙잭션 중 최소값은 얼마인가?

Optional<Transaction> smallestTransaction =
        transactions.stream()
                    .reduce((t1,t2)->
                          t1.getValue() < t2.getValue() ? t1:t2);
Optional<Transaction> smallestTransaction =
        transactions.stream()
                    .min(Comparator.comparing(Transaction::getValue));

4.7 숫자형 스트림

메뉴의 칼로리 합계 계산

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

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

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

위 코드처럼 sum 매서드를 직접 호출할 수 없다. map 매서드가 Stream를 생성하기 때문이다.

스트림의 요소 형식은 Integer지만 인터페이스에는 sum 매서드가 없다.
menu처럼 Stream 형식의 요소만 있다면 sum이라는 연산을 수행할 수 없기 때문이다.

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

기본형 특화 스트림

int요소에 특화된 IntStream, double요소에 특화된 DoubleStream,long 요소에 특화된 LongStream을 제공한다.

숫자 스트림으로 매핑

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

int calories = menu.stream() // Stream<Dish> 반환
                      .mapToInt(Dish::getCalories) //intStream 반환
                        .sum();

mapToInt 매서드는 각 요리에서 모든 칼로리(Integer 형식)를 추출한 다음에 IntStream을 반환한다. 따라서 IntStream인터페이스에서 제공하는 sum매서드를 이용해서 칼로리 합계를 계산할 수 있다.

객체 스트림으로 복원하기

숫자 스트림을 만든 다음에, 원상태인 특화되지 않은 스트림으로 복원할 수 있을까? IntStream의 map연산은 'int를 인수로 받아서 int를 반환하는 람다'를 인수로 받는다. 하지만 정수가 아닌 Dish같은 다른 값을 반환하고 싶으면 어떻게 해야할까?

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

기본값 : OptionalInt

OptionalInt를 이용해서 IntStream의 최댓값 요소를 찾을 수 있다.

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

숫자 범위

IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
range 매서드 : 시작값과 종료값이 결과에 포함되지 않는다.
rangeClosed : 시작값과 종료값이 결과에 포함된다.

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

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)})

                        );


pythagoreanTriples.limit(5)
                        .forEach(t->
                            System.out.println(t[0]+","+t[1]+","+t[2]));

개선할 점?

위 코드에서는 제곱근을 두 번 계산한다. 따라서 (aa,bb,aa+bb) 형식을 만족하는 세 수를 만든 다음에 우리가 원하는 조건에 맞는 결과만 필터링하는 것이 더 최적화된 방법이다.

Stream<double[]> pythagoreanTriples2 = 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)); //세수의 세번째 요소는 만드시 정수이여야 한다.

      pythagoreanTriples2.limit(5)
          .forEach(t->
              System.out.println(t[0]+","+t[1]+","+t[2]));

5.8 스트림 만들기

값으로 스트림 만들기

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이 될 수 있는 객체로 스트림 만들기

null 이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 매소드가 추가되었다.

때로는 null이 될 수 있는 객체를 스트림(객체가 null이라면 빈 스트림)으로 만들어야 할 수 있다. System.getProperty는 제공된 키에 대응하는 속성이 없으면 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)));

배열로 스트림 만들기

int[] numbers = {2,3,5,7,11,13};
int sum = Arrays.stream(numbers).sum(); // 합계는 41

파일로 스트림 만들기

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

uniqueWords = lines.flatMap(line->Arrays.stream(line.split(" "))) //고유 단어 수 계산
                          .distinct() //중복 제거
                          .count(); //단어 스트림 생성

함수로 무한 스트림 만들기

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

iterate 매서드

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(10)
                  .map(t->t[0])
                  .forEach(System.out::print);

iterate 메소드는 프레디케이트를 지원한다. 예를 들어 0에서 시작해서 100보다 크면 숫자 생성을 중단하는 코드를 다음과 같이 구현할 수 있다.

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

generate 메서드

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

Stream.generate(Math::random)
          .limit(5)
        .forEach(System.out::println);
반응형
LIST