• Home
  • About
    • Ara Jo photo

      Ara Jo

      Aspiring Backend Developer :)

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

Book/모던자바인액션/ 5장. 스트림 활용

04 Mar 2021

Reading time ~3 minutes

이번에는 4장에서 살펴본 스트림 API가 지원하는 다양한 연산을 살펴보자.

필터링

프레디케이트로 필터링

스트림의 filter 메서드는 프레디케이트 를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

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

고유 요소 필터링

스트림의 distinct 메서드는 고유 요소로 이루어진 스트림을 반환한다. 즉, 중복을 제거 할 때 사용할 수 있다.

numbers.stream().filter(i -> i % 2 == 0)
        .distinct().forEach(System.out::println);

슬라이싱

프레디케이트로 슬라이싱

filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용하게 된다. 하지만, 만약 이미 오름차순으로 정렬된 리스트라면 반복 작업을 줄일 수 있지 않을까? 이런 경우에 takeWhile을 사용할 수 있다.

List<Dish> slicedMenu1 = specialMenu.stream()
                            .takeWhile(dish -> dish.getCalories() < 320)
                            .collect(toList());

그렇다면 조건에 해당하지 않는 나머지 요소를 선택하려면 어떻게 할 수 있을까? dropWhile을 사용해보자.

List<Dish> slicedMenu2 = specialMenu.stream()
                            .dropWhile(dish -> dish.getCalories() < 320)
                            .collect(toList());

dropWhile은 takeWhile과 정반대의 작업을 수행한다. 처음으로 거짓이 되는 지점까지의 요소를 버리고, 그 지점에서 작업을 중단하고 남은 요소를 반환한다.

스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.

List<Dish> dishes = specialMenu.stream()
                        .filter(dish -> dish.getCalories() > 300)
                        .limit(3)
                        .collect(toList());

위의 코드는 프레디케이트와 일치하는 처음 세 요소를 선택한 뒤 즉시 반환한다. 정렬되지 않은 스트림에도 limit을 사용할 수 있다. (limit의 결과도 정렬되지 않은 채 반환된다.)

요소 건너뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. 만약 n개 이하의 요소를 갖는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다. limit(n)과 skip(n)은 상호 보완적인 연산을 수행한다.

List<Dish> dishes = menu.stream()
                        .filter(d -> d.getCalories() > 300)
                        .skip(2)
                        .collect(toList());

매핑

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

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 스트림의 각 요소에 적용된다.

List<String> dishNames = menu.stream()
                            .map(Dish::getName)
                            .collect(toList());
//단어 리스트가 주어졌을 때 각 단어의 글자수를 반환하는 코드
List<Integer< wordLengths = words.stream()
                                .map(String::length)
                                .collect(toList());

스트림 평면화

map을 응용해서 리스트에서 고유 문자로 이루어진 리스트를 반환해보자. 리스트의 각 단어를 문자로 매핑한 다음에 distinct로 중복 문자를 제거하면 해결되지 않을까?

words.stream().map(word -> word.split(""))
              .distinct().collect(toList());

하지만 이 코드에서 map으로 전달한 람다는 각 단어의 String[]을 반환하기 때문에 map메소드가 반환한 형식은 Stream<String[]>이다. flatMap이라는 메서드를 이용하면 이 코드를 Stream<String>을 반환하도록 수정할 수 있다.

map과 Arrays.stream 활용

우선 배열 스트림 대신 문자열 스트림이 필요하다.

String[] arrayOfWords = {"Goodbye", "World"};
Stream<String> streamOfwords = Arrays.stream(arrayOfWords);
words.stream().map(word -> word.split("")) //각 단어를 개별 문자열 배열로 변환
              .map(Arrays::stream) //각 배열을 별도의 스트림으로 생성
              .distinct()
              .collect(toList());

하지만, 이 코드는 엄밀히 따지면 List<Stream<String>>가 만들어지면서 문제가 해결되지 않았다.

flatMap 사용

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

flatMap은 map(Arrays::Stream)과 달리 하나의 평면화된 스트림을 반환한다. 즉, flatMap은 스트림의 각 값을 다른 스트림으로 만든 뒤, 모든 스트림을 하나의 스트림으로 연결한다.

검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 스트림 API로는 allMatch, anyMatch, noneMatch, findFirst, findAny 등이 있다.

프레디케이트가 적어도 한 요소와 일치하는지 확인

anyMatch를 이용하면 적어도 한 요소와 일치하는지 확인할 수 있다., anyMatch는 boolean을 반환한다.

if(menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) veegetarian friendly!!");
}

프레디케이트가 모든 요소와 일치하는지 검사

allMatch를 이용하면 모든 요소와 일치하는지 확인할 수 있다. 역시, boolean을 반환한다.

boolean isHealthy = menu.stream()
                        .allMatch(dish -> dish.getCalories() < 1000);

프레디케이트와 일치하는 요소가 없는지 확인

noneMatch는 allMatch와 반대 연산을 수행한다. 즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.

boolean isHealthy = menu.steram()
                        .noneMatch(d -> d.getCalories() >= 1000);

요소 검색

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

Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian)
                          .findAny();

Optional이란?

Optional<T>는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. 만약, findAny를 통해 null이 반환된다면 에러를 일으킬 수 있으므로 Optional<T>를 사용하는 것이 좋다.

첫 번째 요소 찾기

논리적인 아이템 순서가 정해져 있는 일부 스트림에서는 첫 번째 요소를 반환할 때 findFirst를 사용할 수 있다.

  • findFirst vs findAny
    findAny도 임의의 요소 한 개를 반환한다는 점은 같지만, 스트림의 병렬성 때문에 순서가 상관없다면 제약이 적은 findAny를 사용한다.

리듀싱

요소의 합

reduce를 이용하면 반복된 패턴을 추상화할 수 있다. 다음 예시 코드를 살펴보자

//스트림의 모든 요소를 더하기
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
//스트림의 모든 요소를 곱하기
int product = numbers.stream().reduce(1, (a, b) -> a * b);

//메서드 참조를 이용한다면?
int sum = numbers.stream().reduce(0, Integer::sum);

최댓값과 최솟값

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

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

이 때 2.에 두 요소에서 최댓값을 반환하는 람다를 넣어주면 최댓값을 구할 수 있다.

//최댓값 구하기
Optional<Integer> max = numbers.stream().reduce(Integer::max);
//최솟값 구하기
Optional<Integer> min = numbers.stream().reduce(Integer::min);


bookmodern_java_in_action Share Tweet +1