📗 개발자 책 읽기/모던 자바 인 액션

[모던자바인액션] chapter 5. Stream 사용 가이드 - 스트림 활용

민돌v 2022. 9. 5. 01:40
728x90

 

" Modern Java In Action - 모던 자바 인 액션, 전문가를 위한 자바 8, 9, 10 기법 가이드 "
독서 스터디 후, 책 내용을 정리한 글입니다.


1) Chapter 1. 자바 8, 9, 10, 11 : 무슨일이 일어나고 있는가?
2) Chapter 2. 동작 파라미터화 코드 전달하기
3) Chapter 3. 람다 표현식
4) Chapter 4. Stream
5) Chapter 5. Stream 활용
6) Chapter 6. Stream으로 데이터 수집하기 
7) Chapter 7. Stream 병렬 데이터처리와 성능

 

 

 

Chapter 5 에서는 스트림 활용 가이드를 제공합니다.

 

그냥 스트링 어떻게 잘 쓰는지에 대한 예시들? 을 제공해줍니다


 

1. 필터링

스트림에서 요소를 선택하는 필터링을 제공합니다.

필터링에는 프리디케이트 필터링 방법과 고유 요소만 필터링하는 방법이 있습니다.

 

1)  Predicate 필터링

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

 

Stream Predicate 예시

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

 


2) 고유 요소 필터링 (Distinct)

스트림은 중복된 요소를 제거하고 고유 요소로 이루어진 스트림을 반환하는 distinct 메소드도 지원합니다.

  • 고유 여부는 스트림에서 만든 객체의 hashCode, equals 로 결정합니다.

 

Stream Distinct 예시

List<Integer> numbers = Arrays.asList(1,2,3,4,5,6,7,8,123,41,5,1,2,3,4,5,5,6,7,8)
        .stream()
        .filter(number->number%2==0)
        .distinct()
        .collect(Collectors.toList());

 


 

2. 스트림 슬라이싱

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

 

1) Predicate를 이용한 슬라이싱

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

 

📌 TAKEWILE

  • takeWile 메소드는, predicate를 만족하지 않는  첫 번째 지점부터 마지막 스트림을 버립니다.

 

Stream takeWhile 예시

// 전체 리스트 순회
List<Dish> filterMenu = specialMenu.stream()
                            .filter(dish -> dish.getCalories() < 320)
                            .collect(toList());
        
// 조건에 맞지 않으면 Stop
List<Dish> filterMenu = specialMenu.stream()
                            .takeWhile(dish -> dish.getCalories() < 320)
                            .collect(toList());

 

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

 

 

📌 DROPWHILE

  • dropWhile메소드는, takeWhile 과 정반대의 결과를 추출합니다.
  • 첫 번째 요소부터, predicate를 처음으로 만족하는 지점까지 버리고 나머지를 반환합니다.

 

Stream dropWhile 예시

// 처음으로 거짓이 되는 지점까지 버림.
List<Dish> filterMenu = specialMenu.stream()
                            .dropWhile(dish -> dish.getCalories() < 320)
                            .collect(toList());

 

dropWhile 은 프리디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버립니다.

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

 

 


 

2) 스트림 축소

  • limit(n) : 주어진 값 이하의 크기를 가지는 새로운 스트림을 반환하는 스트림 메소드입니다.

Stream limit예시

// 3개 요소 반환
List<Dish> filterMenu = specialMenu.stream()
                            .filter(dish -> dish.getCalories() < 320)
                            .limit(3)
                            .collect(toList());

limit 은 조건에 맞는 요소가 채워지면 그 즉시 스트림을 반환합니다.


 

3) 스트림 요소 건너띄기

  • Skip(n) : 스트림은 처음  n 개의 요소를 제외한(스킵한) 스트림을 반환하는 메소드를 지원합니다.
  • limit(n) 과 정반대의 결과를 도출합니다.
  • n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환됩니다.
 

Stream skip 예시

  // 처음 3개 요소 건너뛰기
  List<Dish> filterMenu = specialMenu.stream()
                            .filter(dish -> dish.getCalories() < 320)
                            .skip(3)
                            .collect(toList());

 

 


 

3. 스트림 매핑

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

 

 

1) Stream Map

  • 스트림은 함수를 인수로 받는 map 메소드를 지원합니다.
  • 인수로 제공된  함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑됩니다.
  • 이 과정은 요소를 고친다는 개념보다, 새로운 버전을 만든다라는 개념에서 Trans(변환) 보다 mapping(매핑) 으로 지어졌다고합니다.

 

Stream map 예시

    // 요리명을 추출하는 예제
    List<String> dishNames = menu.stream()
                            .map(Dish::getName)
                            .collect(toList());
                            
                            
    // 단어 리스트의 글자수 추출
    List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
    List<Integer> wordLengths = words.stream()
          .map(String::length)
          .collect(toList());


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

 


 

2) Stream FlatMap (스트림 평면화)

 

  •  flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행합니다.

 

예를들어, list 혹은 배열의 요소들을 각각의 스트림으로 만든 후, 이 만들어진 모든 스트림 요소를 하나의 스트림으로 만들어 어떤 작업을 진행하고 싶을때 flatMap을 사용합니다.

 

Stream flatmap 예시

  • 예를들어 [“hello”, “world”] 리스트가 [“h”, “e”, “l”, “l”, “o”, “w”, “o”, “r”, “l”, “d”] 가 되도록 변경
  List<String> uniqueCharacters = words.stream()
          .map(word -> word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 변환 
          .flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
          .distinct()
          .collect(toList());

 

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

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

 

 


 

4. 스트림 검색과 매칭

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

 

📌 allMatch

  • Predicate가 모든 요소와 일치하는지 검사
boolean isMatch = menu.stream().allMatch(Dish -> dish.getCalories() < 1000)

 

📌 anyMatch

  • Predicate가 적어도 한 요소와 일치 하는지 확인
boolean isMatch = menu.stream().anyMatch(Dish -> dish.getCalories() < 1000)

 

📌 NoneMatch

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

 

 

📌 요소검색 (findAny, findFirst)

findAny

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

 

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. 리듀싱

Stream API 는 마치 Sql 처럼 모든 스트림 요소의 값을 처리해서 결과를 도출하는 Reduce 메소드를 제공합니다.

 

  • reduce 메소드의 연산 과정은, 각 요소의 결과에 반복적으로 더해집니다,
  • 그렇기 때문에 reduce를 사용하면 반복된 패턴을 추상화 할 수 있습니다.
//초깃값 0.
// 두 요소를 조합해 새로운 값을 만드는 BinaryOperator. 예제에서는 람다 표현식 (a, b) -> a + b를 사용했다.
//reduce로 다른 람다, 즉 (a, b) -> a * b를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);

 

Java 8 에서는 Integer 클래스에 두 숫자를 더하는 정적 sum 메서드를 지원하여 아래와 같이 사용할 수 있습니다.

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

 

👏 초기값을 없앨수도 있습니다.

초깃값을 받지 않도록 오버로드된 reduce는 결과값으로 Optional객체를 반환합니다.

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

 

👏 Stream 최대 값, 최솟 값

최대값과 최솟값을 찾는 연산도 reduce를 활용하면 쉽게 할 수 있습니다.

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

 

 


 

6. 조금 더 우아한 레듀싱

Stream 레듀스를 사용하면 질의문 처럼 사용할 수 있지만 더 간단하고 명시적으로 사용하고 싶을 때가 있습니다.

 

1) 기본형 특화 레듀싱

ex) 객체로 변환하고 reduce를 사용해서 sum을 하는 과정

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

 

Stream에서는 기본형 특화 스트림을 제공해서 아래처럼 조금 더 명시적이고 간편하게 기본형 특화 메소드들을 사용할 수 있도록 합니다.

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

ex) mapToInt, mapToDouble, mapToLong

 


2) OptionalInt

기본값이 없는 Stream 연산에서 최대 최솟값을 구할 떄, 이를 구별하기위해 기본형 특화 스트림 + Optional 기능을 제공합니다.

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

 

물론 Int 형 뿐만아니라 다른 기본형 특화 스트림 타입에도 적용이 가능합니다.

 

 


 

7. 스트림 만들기

다양한 방법과 조건으로 스트림을 생성할 수 있습니다.

 

1. 값으로 스트림 만들기

Stream.of(“Modern “, “Java”, “In”, “Action”);

 

2. null 가능한 스트림 만들기 - Java 9이상 부터

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

 

 

3. null이 될 수 있는 객체를 포함하는 스트림 값을 flatMap을 사용한다면 더욱 유용하게 사용할 수 있습니다.

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

 

 

4. 배열로 스트림 만들기

  int[] nubmers = {1, 2, 3, 4, 5};
  int sum = Arrays.stream(numbers).sum();
 

 

5. 함수로 무한 스트림 만들기

  • iterate 사용
  Stream.iterate(0, n -> n + 2)
        .limit(10)
        .forEach(System.out::println)
  • iterate 메서드는 초깃값과 람다를 활용하여 스트림을 생성 할 수 있다. 이때 생성되는 스트림은 무한적이기 때문에 반드시 limit과 같은 메서드를 사용하여 범위를 지정해주어야 한다.
  • iterate는 순차적으로 실행되며 무한적으로 새엇ㅇ되는 스트림을 언바운드 스트림이라고 표현한다.
  • Java 9 부터는 iterate 에서 Predicate를 지원한다.
     

 

  • generate 사용
    • iterate와 비슷하게 generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다.
    • iterate와 차이점은 생산된 값을 연속적으로 계산하지 않고 Supplier 를 인수로 받아 새로운 값을 생성한다.
       
  Stream.generate(Math::random)
        .limit(5)
        .forEach(System.out::println)

 

 

 

 

 

반응형