Modern Java in Action - CH5 스트림 활용

AIR·2024년 4월 23일
0

Modern Java in Action

목록 보기
5/5

스트림 API가 지원하는 다양한 연산

데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다. 따라서 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다. 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현하는 외부 반복으로는 달성할 수 없다.

필터링

프레디케이트로 필터링

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

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

고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다. 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
        .filter(i -> i % 2 == 0)  //2, 2, 4
        .distinct()  //중복 제거
        .forEach(System.out::println);  //2, 4 출력

스트림 슬라이싱

프레디케이트를 이용한 슬라이싱

자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다. 칼로리가 오름차순으로 정렬된 리스트가 있을 때 특정 칼로리 이하의 요리를 스트림으로 나타낸다고 할 때 다음의 코드는 올바른 결과를 나타내지만 정렬이 돼있기 때문에 어느 시점부터는 반복 작업이 불필요해진다.

List<Dish> filteredMenu = specialMenu.stream()
        .filter(dish -> dish.getCalories() < 320)  //전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용
        .collect(toList());

이때 takeWhile 연산을 이용하면 도중에 반복 작업을 중단해 스트림을 슬라이스할 수 있다.

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

나머지 요소를 선택하려면 dropWhile을 이용할 수 있다. dropWhile은 takeWhile과 정반대의 작업을 수행하며 프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. 그리고 무한 스트림에서도 동작한다.

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

스트림 축소

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 다음 코드는 프레디케이트와 일치하는 처음 세 요소를 선택한 다음 즉시 결과를 반환한다.

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

요소 건너뛰기

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.

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

매핑

특정 객체에서 특정 데이터를 선택하는 데이터 처리 과정에서 자주 수행되는 연산이다. 스트림 API의 map과 flatMap 메서드가 그 기능을 제공한다.

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

스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.

List<Integer> dishNameLengths = menu.stream()
        .map(Dish::getName)   //Stream<String>
        .map(String::length)  //Stream<Integer>
        .collect(toList());

스트림 평면화

["Hello", "Wolrd"]란 리스트를 고유 문자로 이루어진 리스트로 반환해본다. 결과값은 ["H", "e", "l", "o", "W", "r", "d"]이 된다.

List<String> words = Arrays.asList("Hello", "World");

배열을 문자열 스트림으로 반환하기 위해 Arrays.stream() 메서드를 이용한다. 하지만 이렇게 코드를 작성할 경우 각 배열을 스트림으로 만들기 때문에 결국 스트림 리스트가 반환된다.

List<Stream<String>> wrongResult = words.stream()
        .map(word -> word.split(""))  //각 단어를 문자열 배열로 반환
        .map(Arrays::stream)  //각 배열을 별도의 스트림으로 생성
        .distinct()
        .collect(toList());

flatMap을 사용하면 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, 각 스트림을 하나의 평면화된 스트림으로 반환한다.

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

다음 코드는 flatMap을 이용하여 두 숫자 리스트를 모든 숫자 쌍의 리스트를 반환한다.

List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(3, 4);
List<int[]> result = list1.stream()
        .flatMap(i -> list2.stream()
                .map(j -> new int[]{i, j})
        )
        .collect(toList());

검색과 매칭

프레디케이트가 적어도 한 요소와 일치

boolean isVegetarian = menu.stream()
        .anyMatch(Dish::isVegetarian);

프레디케이트가 모든 요소와 일치

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

noneMatch는 allMatch와 반대 연산을 수행한다. isHealthy를 다음과 같이 구현할 수 있다.

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

쇼트서킷

anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다. 이러한 연산은 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있다. 원하는 요소를 찾았으면 즉시 결과를 반환한다. 마찬가지로 limit도 해당되며 무한한 요소를 가진 스트림을 유한한 크기로 줄일 수 있는 유용한 연산이다.

요소 검색

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

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

Optional

Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. 위의 findAny는 아무 요소도 반환하지 않을 수 있는데 null은 쉽게 에러를 일으킬 수 있으므로 Optional을 이용한다.

  • isPresent(): Optional이 값을 표함하면 true
  • ifPresent(Consumer<T> block)은 값이 있으면 주어진 블록을 실행
  • T get()은 값이 존재하면 값을 반환하고, 없으면 NoSuchElementException
  • T orElse(T other)는 값이 있으면 값을 반환, 없으면 기본값을 반환
menu.stream()
        .filter(Dish::isVegetarian)
        .findAny()
        .ifPresent(dish -> System.out.println(dish.getName()));

첫 번째 요소 찾기

리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾기 위해 findFirst() 메서드를 사용할 수 있다.

Optional<Integer> firstSquareDivisibleByThree = 
        oneToFive.stream()
        .map(n -> n * n)
        .filter(n -> n % 3 == 0)
        .findFirst();  //9

findFirst와 findAny

findFirst와 findAny 메서드가 필요한 이유는 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

리듀싱

리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출하는 연산이다.

요소의 합

List<Integer> numbers = List.of(4, 5, 3, 9);
int sum = 0;
for (int number : numbers) {
    sum += number;
}

위와 같이 numbers 배열의 합을 구하는 for-each 코드가 있을 때 이를 reduce를 이용해서 표현할 수 있다.

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

초깃값은 0이 되고 람다의 첫 번째 a에 0이 사용되고 b에서 4이 사용되어 0+4=4이 새로운 누적값이 된다. 누적값으로 람다를 다시 호출하여 다음 요소인 5를 소비하여 결과는 9이 된다. 이런 식으로 마지막 요소를 호출할 때까지 반복한다.

메서드 참조를 이용하면 코드를 좀 더 간결하게 만들 수 있다.

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

초깃값 없음

초깃값을 받지 않도록 오버로드된 reduce도 있는데 이때는 Optional 객체를 반환한다.

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

최댓값과 최솟값

reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행하므로 최댓값과 최솟값도 찾을 수 있다.

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

맵 리듀스

map과 reduce를 연결하는 기법을 map-reduce 패턴이라 하며, 쉽게 병렬화하는 특징이 있다.

int count = menu.stream()
        .map(dish -> 1)
        .reduce(0, Integer::sum);

count() 메서드와 동일한 결과를 가진다.

long count = menu.stream()
		.count();
profile
백엔드

0개의 댓글