Post

2번 읽는 Modern Java In Action - Chapter05 스트림활용(1)

🔅 개요

스트림 API가 지원하는 다양한 연산들을 다뤄본다고 한다.

참고로 여기서 나오는 menu리스트들은

1
2
3
4
5
6
7
8
9
@BeforeEach
void init() {
    menu = Arrays.asList(
            new Dish("chicken", false, 200, Dish.Type.MEAT),
            new Dish("pork Chop", false, 400, Dish.Type.FISH),
            new Dish("pork Loin", false, 230, Dish.Type.FISH),
            new Dish("salad", true, 199, Dish.Type.OTHER)
    );
}

이렇게 구현되어있다.

🔅 필터링

  • filter메서드는 프레디케이트를 인수로 받아서 해당 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
1
2
3
4
5
6
7
8
9
@Test
@DisplayName("프리디게이트로 필터링")
void testPredicateFilter(){
    List<Dish> vegetarianMenu = menu.stream()
            .filter(Dish::isVegetarian)
            .collect(Collectors.toList());
    //salad
    vegetarianMenu.stream().forEach(System.out::println);
}
  • distinct()를 통한 중복제거도 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("프리디게이트로 필터링 - 중복제거")
void testPredicateDistinctFilter() {
    List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
    //중복제거
    //2, 4
    numbers.stream()
            .filter(i -> i % 2 == 0)
            .distinct()
            .collect(Collectors.toList())
            .forEach(System.out::println);
}

🔅 스트림 슬라이싱

만약 칼로리순으로 정렬된 경우이고, 320칼로리 이하의 요리를 선택해야한다면 어떻게 할 수 있을까

1
2
3
4
5
6
7
8
9
10
11
List<Dish> sortedMenu = Arrays.asList(
                new Dish("fruit", true, 120, Dish.Type.OTHER),
                new Dish("prwans", false, 300, Dish.Type.FISH),
                new Dish("rice", false, 350, Dish.Type.OTHER),
                new Dish("chicken", false, 400, Dish.Type.MEAT),
                new Dish("french fries", true, 530, Dish.Type.OTHER)
        );

sortedMenu.stream()
                .filter(dish -> dish.getCalories() < 320)
                .collect(Collectors.toList());

이런식으로 할 수 있지만, 정렬이 되었음에도 불구하고 4번째인 400칼로리도 filter가 적용될 것이고 5번째인 800칼로리도 마찬가지로 돌것이다. 그렇게 무의미하게 필터가 계속 작용될 것이다.

Java9버전에서는 taskWhile()를 통해 정렬된 값에 대한 필터를 적용하여 범위를 벗어난값이 나타나면 연산을 종료시킬 수 있다.

1
2
3
4
5
List<Dish> result = sortedMenu.stream()
        .takeWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());
//fruit, prawns
result.stream().forEach(System.out::println);

이런식으로 해결할 수 있다.

반대로 320칼로 이상의 요소를 선택하려면 어떻게 해야할까?

1
2
3
4
5
6
List<Dish> result = sortedMenu.stream()
        .dropWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());

//rice, chicken, french fries
result.stream().forEach(System.out::println);

dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. 그리고 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다.

limit(n)를 이용해 스트림 최대 요소 n개를 반환할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
@DisplayName("limit를 이용한 테스트")
void testLimit(){
    List<Dish> sortedMenu = Arrays.asList(
            new Dish("fruit", true, 120, Dish.Type.OTHER),
            new Dish("prwans", false, 500, Dish.Type.FISH),
            new Dish("rice", false, 350, Dish.Type.OTHER),
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER)
    );

    List<Dish> dishes = sortedMenu.stream()
            .filter(dish -> dish.getCalories() > 300)
            .limit(3)
            .collect(Collectors.toList());
    
    // prwans
    // rice
    // chicken
    dishes.stream().forEach(System.out::println);
}

skip(n)을 통하여 스트림 요소 n개를 건너 뛸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@BeforeEach
void init() {
    menu = Arrays.asList(
            new Dish("chicken", false, 200, Dish.Type.MEAT),
            new Dish("pork Chop", false, 400, Dish.Type.FISH),
            new Dish("pork Loin", false, 230, Dish.Type.FISH),
            new Dish("salad", true, 199, Dish.Type.OTHER)
    );
}

@Test
@DisplayName("skip을 이용한 테스트")
void testSkip(){
    List<Dish> dishes = menu.stream()
            .filter(d -> d.getCalories() < 300)
            .skip(2)
            .collect(Collectors.toList());
    //salad
    dishes.stream().forEach(System.out::println);
}

🔅 매핑

특정객체의 특정 데이터를 선택하는 작업이 필요할 때도 있을 것이다. 이는 mapflatMap메서드가 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("map 테스트")
void testMap(){
    List<String> dishNames = menu.stream()
            .map(Dish::getName)
            .collect(Collectors.toList());
    // chicken
    // pork Chop
    // pork Loin
    // salad
    dishNames.stream().forEach(System.out::println);
}

Dish::getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 **Stream** 형식을 갖는다.

만약 각 요리명의 길이를 알고 싶으면 어떻게 할까? map을 중첩해서 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
@Test
@DisplayName("map 중첩 테스트")
void testMapDuplicate(){
    List<Integer> dishNameLengths = menu.stream()
            .map(Dish::getName)
            .map(String::length)
            .collect(Collectors.toList());
    //7 9 9 5 
    dishNameLengths.stream().forEach(System.out::println);
}

🔅 스트림 평면화

책에서 flatMap은 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다는데, 이해가 잘 되지 않는다.

만약 [“Hello”, “World”]를 [“H”,”e”,”l”,”l”,”o”,”W”,”o”,”r”,”l”,”d”]로 쪼개고 싶다고 하자.

그러면

1
2
3
4
5
6
List<String> words = Arrays.asList("Hello", "World");

        //평면화 전
List<String[]> mapResult = words.stream()
        .map(word -> word.split(""))
        .collect(Collectors.toList());

결과는 [[“H”,”e”,”l”,”l”,”o”],[“W”,”o”,”r”,”l”,”d”]] 로 나올 것이다.

우리가 원하는 건 2개의 배열에 결과를 담는게 아니다.

flatMap은 이러한 작업을 도와주는데, 스트림을 1차원 평면화 시킨다고 보면된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("스트림 평면화")
void testMapDistinct(){
List<String> words = Arrays.asList("Hello", "World");

List<String> result = words.stream()
        .map(word -> word.split(""))
        .flatMap(Arrays::stream) // 배열을 스트림으로 변환
        .collect(Collectors.toList());

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

🔅 검색과 매칭

검색하는데 용이한 메서드들을 제공한다는 것이다.

anyMatch

프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 사용한다.

1
2
3
4
5
6
7
@Test
@DisplayName("anyMatch 테스트")
void testAnyMatch(){
        if(menu.stream().anyMatch(Dish::isVegetarian)){
                System.out.println("is vegetarian");
        }
}

allMatch

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

1
2
3
4
5
6
7
@Test
@DisplayName("allMatch 테스트")
void testAllMatch(){
        if(menu.stream().allMatch(dish -> dish.getCalories() < 1000)){
                System.out.println("모든 음식의 칼로리가 1000보다 적습니다.");
        }
}

noneMatch

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

1
2
3
4
5
6
7
@Test
@DisplayName("noneMatch 테스트")
void testNoneMatch(){
        if(menu.stream().noneMatch(dish -> dish.getCalories() > 1000)){
                System.out.println("모든 음식의 칼로리가 1000을 초과하지 않습니다.");
        }
}

findAny

스트림에서 임의의 요소를 반환한다. 예제에서는 자료구조의 양이 적어서 매 번 임의의 값이 나오지는 않는다.

1
2
3
4
5
6
7
8
9
@Test
@DisplayName("findAny 테스트")
void testFindAny(){
        Optional<Dish> dish = menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();
        //Optional[salad]
        System.out.println(dish);
}

Optional에 관한 내용은 10챕터에 자세히 나온다니, 생략하도록 하겠다. 간단히 null을 처리하기 위해 등장한 클래스다.

findFirst

스트림에서 첫 번째 요소를 찾기위해 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("findFirst 테스트")
void testFindFirst(){
        List<Integer> numbers = Arrays.asList(1,2,3,4,5);

        numbers.stream()
                .map(n -> n * n)
                .filter(n -> n % 3 == 0)
                .findFirst()
                //9출력
                .ifPresent(i -> System.out.println(i));
}

병렬 실행에서는 첫 번째 요소를 반환하는 findFirst를 제대로 이용하기 어렵다. 때문에 findAny를 사용하는게 좋다.

내용이 길어서 2개로 나눠서 작성하게 되었다.

This post is licensed under CC BY 4.0 by the author.