Chapter 5 - 스트림 활용(1편)

2022. 9. 4. 11:05개발 관련 책 읽기/모던 자바 인 액션

✅ 아래 내용들에 대해서 알아보자

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

 

모든 실습 내용은 깃허브(아래 링크)에 있습니다. 참고 부탁드립니다 😀😀

https://github.com/underdarks/ModernJavaInAction-TIL/tree/main/src/test/java/modernjavainaction/practice

 


필터링

1. 프레디케이트 필터링

 아래의 코드는 프레디케이트(불리언을 반환하는 함수)를 인수로 받아서 채식주의자 음식 필터링하는 예제 코드이다.

여기서 주요하게 볼 코드는 filter를 사용하여 boolean Type을 반환하는 isVegetarian 메서드를 호출하여

채식주의자 음식만 필터링하는 부분이다.(아래 그림 1 참고)

@DisplayName("프레디케이트(불리언을 반환하는 함수)을 활용한 채식주의자 음식 필터링 하기")
@Test
public void FilterByVegetarianDishWithPredicate() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    List<Dish> vegetarianDishes = dishes.stream()
            .filter(Dish::isVegetarian)             //프레디케이트(불리언을 반환하는 함수) 필터링
            .collect(toList());

    //then
    for (Dish vegetarianDish : vegetarianDishes) {
        System.out.println("vegetarianDish = " + vegetarianDish);
    }

    assertThat(vegetarianDishes.size()).isGreaterThan(0);
}

 

 

그림 1. Filter 과정

 

2. 고유 요소 필터링

아래의 코드는 고유 요소로 이루어진 스트림을  filter와 distinct를 활용하여 짝수인 요소만 반환하는 코드이다.

 

@DisplayName("고유 요소로 이루어진 스트림 짝수 필터링")
@Test
public void filterEvenNumber() {
    //given
    List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);

    //when
    List<Integer> evenNumbers = numbers.stream()
            .filter(num -> num % 2 == 0)
            .distinct()         //중복 제거
            .collect(toList());

    //중복 제거 된 짝수 출력
    evenNumbers.stream()
            .forEach(System.out::println);

    //then
    assertThat(evenNumbers.size()).isEqualTo(2);
}

 


스트림 슬라이싱

1.TakeWhile 활용

 filter()는 모든 요소를 다 확인하여 조건에 대해 확인하지만

takeWhile()은 해당 조건에 대해 적합하지 않으면 연산을 중단하게 된다.

 

이러한 특징은 데이터 소스 양이 많을 때 O(n) 시간 복잡도를 다 돌지 않게 되어 연산 속도가 유리한 장점이 있다.

하지만 takeWhile()을 사용하기 위해서는 데이터 소스가 정렬된 상태 이어야 사용할 수 있는 조건이 있다.

 

/**
 * Filter와 TakeWhile의 차이는 Filter는 모든 요소를 다 확인하지만 TakeWhile은 해당 요소가 조건에 대해 참이 아닐 경우 바로 거기서 연산을 멈춘다
 * takewhile은 소스가 정렬되어 있을때 사용하기 좋음
 */
@DisplayName("TakeWhile을 활용한 칼로리 320이하인 요리 찾기")
@Test
public void findUnder320caloriesDishWithTakeWhile() {
    //given
    Collection<Dish> specialMenu = getSpecialMenu();

    //when
    List<Dish> under320CaloriesDishes = specialMenu.stream()
            .takeWhile(dish -> dish.getCalories() <= 320)    //칼로리순으로 정렬되어있기 때문에 해당 요소가 320 칼로리가 넘으면 takewhile 연산은 종료한다. 시간 복잡도를 줄일 수 있는 장점이 생긴다
            .collect(toList());


    for (Dish dish : under320CaloriesDishes) {
        System.out.println("dish = " + dish);
    }

    //then
    assertThat(under320CaloriesDishes.size()).isEqualTo(2);
}

 

2.DropWhile 활용

 dropWhile()은 takeWhile() 메서드와 정반대의 작업을 수행한다.

즉, 처음으로 거짓되는 지점이 발견되면 그 이전의 발견된 요소들을 버린 후 나머지 남은 요소들을 반환한다.

 

dropWhile()도 데이터 소스가 정렬된 상태이어야 한다는 전제조건이 있다.

 

/**
 * DropWhile은 TakeWhile과 정반대의 작업을 수행한다. dropwhile은 프레디케이트가 거짓이 되면 작업을 중단하고 남은 요소를 반환한다
 * dropWhile은 소스가 정렬되어 있을때 사용하기 좋음
 */
@DisplayName("DropWhile을 활용한 칼로리 320이상인 요리 찾기")
@Test
public void findOver320caloriesDishWithDropWhile() {
    //given
    Collection<Dish> specialMenu = getSpecialMenu();

    //when
    List<Dish> over320CaloriesDishes = specialMenu.stream()
            .dropWhile(dish -> dish.getCalories() <= 320)    //칼로리가 320 이상인 음식이 나오면 작업 중단 후 남은 요소들을 반환한다
            .collect(toList());


    for (Dish dish : over320CaloriesDishes) {
        System.out.println("dish = " + dish);
    }

    //then
    assertThat(over320CaloriesDishes.size()).isEqualTo(3);
}

 

3. 스트림 축소

 스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는

limit(n) 메서드를 지원하여 최대 요소 n개를 포함하는 요소를 반환할 수 있다.

 

@DisplayName("스트림 축소")
@Test
public void streamReduction() {
    //given
    Collection<Dish> specialMenu = getSpecialMenu();


    //when
    List<Dish> over300CaloriesDishes = specialMenu.stream()
            .dropWhile(dish -> dish.getCalories() < 300)
            .limit(3)
            .collect(toList());


    for (Dish over300CaloriesDish : over300CaloriesDishes) {
        System.out.println("over300CaloriesDish = " + over300CaloriesDish);
    }


    //then
    assertThat(over300CaloriesDishes.size()).isEqualTo(3);
}

 

그림 2. limit 과정

 

4. 요소 건너뛰기

 스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.

limit(n)은 n개만큼만 요소를 반환하고 skip(n)은 n개를 제외한 요소들을 반환한다는 차이점이 있다.

@DisplayName("요소 건너뛰기")
@Test
public void streamSkip() {
    //given
    Collection<Dish> specialMenu = getSpecialMenu();


    //when
    List<Dish> skipDishes = specialMenu.stream()
            .filter(dish -> dish.getCalories() > 300)
            .skip(2)
            .collect(toList());

    for (Dish skipDish : skipDishes) {
        System.out.println("skipDish = " + skipDish);
    }

    //then
    assertThat(skipDishes.size()).isEqualTo(1);
}

 


매핑

 스트림 API의 map과 flatMap 메서드를 통해 특정 데이터를 선택하는 기능을 제공한다. 각각에 대해서 알아보자

 

1.Map

 스트림은 함수를 인수로 받는 map을 지원한다.

인수로 제공된 함수는 각 요소에 적용되어 새로운 요소로 매핑된다.

다음 예시 1,2 를 보면서 이해해보자.

 

예시 1)

해당 문자열들의 글자수의 리스트를 반환하는 함수이다.

map을 활용하여 length 메서드를 인자로 넘겨 각 문자열들의 길이를 구하게 된다. 

@DisplayName("각 단어가 포함하는 글자 수 반환 - 매핑 활용")
@Test
public void streamMapping() {
    //given
    List<String> words = Arrays.asList("Modern", "Java", "In", "Action");

    //when
    List<Integer> wordSize = words.stream()
            .map(String::length)
            .collect(toList());

    for (Integer size : wordSize) {
        System.out.println("size = " + size);
    }
}

 

예시 2)

각 요리명의 길이를 map() + length()을 활용하여 리스트로 반환하는 코드이다.

@DisplayName("각 요리명의 길이를 반환")
@Test
public void getFoodNameLength() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    List<Integer> lengths = dishes.stream()
            .map(dish -> dish.getName().length())
            .collect(toList());

    //then
    for (Integer length : lengths) {
        System.out.println("length = " + length);
    }
}

 

=> 둘다 동일하게 모든 요소에 map() + length()를 적용하여 문자열의 길이를 구하였다.

 

2.flatMap

flatMap을 활용하여 ["Hello", "World"] 리스트를 ["H", "e", "l", "l", "o", "W", "r", "d"]를 포함하는 리스트로 변환해보자.

아래 예시 코드를 보면서 같이 확인해보자.

 

@DisplayName("스트림 평면화")
@Test
public void streamFlattening() {
    //["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"] 결과값이 나오게 해야한다
    //given
    List<String> words = Arrays.asList("Hello", "World");

    //1번째 시도
    List<String[]> list = words.stream()
            .map(s -> s.split(""))
            .distinct()
            .collect(toList());

    list.forEach(strings -> strings.toString());

    //2번째 시도
    String[] arrayOfWords = {"Hello", "World"};

    List<Stream<String>> collect = Arrays.stream(arrayOfWords)
            .map(word -> word.split(""))
            .map(Arrays::stream)
            .distinct()
            .collect(toList());

    //collect.forEach(System.out::println);

    //3번째 시도(flatMap 사용)
    List<String> result = words.stream()
            .map(s -> s.split(""))                  //각 단어를 개별 문자로 tokenizing("h","e","l" ....)
            .flatMap(strings -> Arrays.stream(strings))   //생성된 스트림을 하나의 스트림으로 평면화
            .distinct()
            .collect(toList());

    result.forEach(System.out::println);

}

 

아래 그림은 위의 1번째 시도의 과정을 그림으로 나타낸것이다.

 

실제 결괏값을 collect메서드로  List<String>을 얻어야 하지만

List<String[]>을 얻어서 우리가 원하는 결괏값을 얻지 못하였다.

 

 

 

아래 그림은 map으로 먼저 각 문자열을 tokenizing 한 후

flatMap메서드를 사용하여 Stream<String[]> -> Stream<String>으로 변환한다.

 

그리고 distinct를 활용하여 중복제거 후 collect으로 List<String>을 얻어 우리가 원하는 결괏값을 도출한다.

여기서 핵심은 flatMap 메서드인데

flatmap은 각 값을 다른 스트림으로 만든 후 모든 스트림을 하나의 스트림으로 합치는 역할을 수행한다.

 

 

그림 3. flatMap 과정


매칭

1.anyMatch

 요소가 하나라도 일치하는지 확인한다. 아래는 음식들 중 채식요리가 적어도 하나라도 있는지 확인한다.

//요소 하나라도 일치하는지 확인(쇼트서킷)
@DisplayName("음식들 중에 채식요리가 적어도 하나라도 있는지 확인")
@Test
public void isVegetarainFoodAtLeastOne() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    boolean result = dishes.stream()
            .anyMatch(Dish::isVegetarian);  //boolean 반환하는 최종연산

    //then
    assertThat(result).isEqualTo(true);
}

 

2.allMatch

 모든 요소가 일치하는지 확인한다. 아래 코드는 모든 음식이 1000칼로리 이하인지 확인한다.

//모든 요소 일치 확인(쇼트서킷)
@DisplayName("모든 음식이 1000칼로리 이하인지 확인")
@Test
public void isAllFoodUnder1000Kcal() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    boolean result = dishes.stream()
            .allMatch(dish -> dish.getCalories() <= 1000);


    //then
    assertThat(result).isEqualTo(true);
}

 

3.noneMatch

 nonematch는 Allmatch와 반대 연산을 수행한다.

즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

아래는 모든 음식이 1000칼로리 이상인지 확인하여 모든 음식이 1000칼로리 이하임을 증명한다.

//모든 요소 불일치 확인(쇼트서킷, <-> AllMatch와 반대 연산)
@DisplayName("모든 음식이 1000칼로리 이상인지 확인")
@Test
public void isAllFoodOver1000Kcal() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    boolean result = dishes.stream()
            .noneMatch(dish -> dish.getCalories() >= 1000);


    //then
    assertThat(result).isEqualTo(true);
}

 


검색

1.findAny

 findAny 메서드는 현재 스트림에서 임의의 요소를 Optional로 반환한다. 

 

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

자바에서 NPE(NullPointError)를 방지하고자 만든 클래스이다. 자세한 건 Optional에 대한 다른 글에서 설명하겠다.

@DisplayName("요소 검색")
@Test
public void ElementSearchByfindAny() {
    //given
    Collection<Dish> dishes = getDishList();

    //when
    dishes.stream()
            .filter(Dish::isVegetarian)
            .findAny()
            .ifPresent(System.out::println);

    //then
}

 

2.findFirst

 findFirst는 리스트 또는 정렬된 연속 데이터에서 첫 번째 요소를 찾기위한 메서드이다.

그러나 findFirst는 병렬 실행에서 첫 번째 요소를 찾기가 어렵다. 따라서 병렬 상황에서는 findAny를 사용한다.

/**
 * findFirst와 findAny는 왜, 언제 사용하나?
 * ->병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 반환 순서가 상관 없다면 Parallel Stream에서는 제약이 적은 findAny가 더 작합하다
 * ->순차 실행에서는 findFirst를 활용하여 조건에 부합한 첫번째 요소를 찾을 때 유용하다
 */
@DisplayName("첫번째 요소 찾기")
@Test
public void findFirstElement() {

    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 6);

    //when
    Integer result = list.stream()
            .map(value -> value * value)
            .filter(value -> value % 3 == 0)
            .findFirst()
            .orElse(0);


    //then
    assertThat(result).isEqualTo(9);
}

리듀싱

 모든 요소들의 합을 구하려면 어떻게 해야할까? 

바로 리듀싱(recude, 모든 스트림 요소를 처리해서 값으로 도출) 연산을 통해 합을 구할수 있다.

리듀싱을 사용 전/후 코드를 살펴보자

 

아래 코드는 리듀싱을 사용하지 않고 스트림의 모든 요소를 더하는 코드이다.

mapToInt로 스트림의 모든 요소를 int로 변환 후 sum 메소드를 사용하여 합을 구하였다.

@DisplayName("리듀싱을 적용하기 전 리스트의 모든 요소 더하기")
@Test
public void sumWithStreamSumMethod() {
    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    //when
    int sum = list.stream()
            .mapToInt(Integer::intValue)
            .sum();


    System.out.println("sum = " + sum);
    //then
    assertThat(sum).isEqualTo(15);
}

 

리듀싱을 적용한 코드를 보자.

리듀싱 적용하기 전 코드에 비해 코드가 짧고 가독성이 좋아보인다.

reduce(0, (a,b) -> a+b)) 람다 코드가 초기값 0을 받아 스트림의 각 요소에 누적으로 합을하여 실제 결과값을 구한다.(0+4+5+3+9 ....)

 

자세한건 아래 그림을 참고하자.

/**
 * 리듀싱
 * -> 모든 스트림 요소를 처리해서 값으로 도출하는 과정
 * -> FP에서는 종이를 작은 조각이 될 떄까지 반복해서 접는 것과 비슿해서 "폴드"라 부름
 */
@DisplayName("리듀싱을 적용 후 리스트의 모든 요소 더하기")
@Test
public void sumWithReducing() {
    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    //when
    /**
     * 파라미터 = 초기값, accumulator(누산기,람다)
     * (1+2) +3) +4) +5) 이런식으로 누적으로 계산되서 마지막 스트림 요소까지 연산한다
     */
    Integer sum = list.stream()
            .reduce(0, (a, b) -> a + b);

    System.out.println("sum = " + sum);

    //then
    assertThat(sum).isEqualTo(15);
}

 

 

 

 

초기값이 없는 recduce 메서느는 Optional 객체를 반환한다.

왜 Optional 객체를 반환할까? 그 이유는, Stream에 아무 요소도 없는 경우 초기값이 없으므로 reduce는 연산을 할 수없다. 그로 인해 Optional 객체로 감싼 결과를 반환한다.

 

@DisplayName("초기값이 없는 리듀싱을 적용 후 리스트의 모든 요소 더하기")
@Test
public void sumWithReducingNotInitValue() {
    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    //when
    Integer sum = list.stream()
            .reduce((a, b) -> a + b)//초기값이 없는 reduce는 Optional을 반환함
            .orElse(0);


    System.out.println("sum = " + sum);
    //then
    assertThat(sum).isEqualTo(15);
}

 

Reduce를 황용하여 최댓값/최소값을 구하는 코드를 보자.

recude() 메서드의 람다식에 Math.max(),min()를 적용하여 최대값/최소값을 구한다.

자세한건 아래 그림을 참고하자

 

@DisplayName("Reduce를 활용한 최대값 구하기")
@Test
public void getMaxValueWithReduce(){
    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    //when
    Integer maxValue = list.stream()
            .reduce(Math::max)
            .orElse(0);


    //then
    assertThat(maxValue).isEqualTo(5);
}

@DisplayName("Reduce를 활용한 최소값 구하기")
@Test
public void getMinValueWithReduce(){
    //given
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);

    //when
    Integer minValue = list.stream()
            .reduce(Math::min)
            .orElse(0);


    //then
    assertThat(minValue).isEqualTo(1);
}

 

 

 

5장은 내용이 많아서 1,2편으로 나누려고 합니다.

2편 보기 =>  2022.11.08 - [개발자 책 읽기/모던 자바 인 액션] - Chapter 5 - 스트림 활용(2편)

반응형