Book/모던 자바 인 액션

[정리] 모던 자바 인 액션 - 스트림 활용(1)

따라쟁이개발자 2023. 1. 7. 10:00

필터링

  • filter(Predicate) : boolean을 반환하는 Predicate를 인수로 받아 일치하는 요소만 포함하는 스트림 반환
  • distinct() : 고유 요소로만 이루어진(=중복 제거된) 스트림 반환
// Predicate로 필터링 : boolean을 반환하는 Predicate을 인수로 받아 일치하는 요소만 필터링
List<Dish> vegetarianMenu = menu.stream()
        .filter(Dish::isVegetarian)
        .collect(toList());

// 고유 요소 필터링 : distinct로 고유 요소 필터링
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
        .filter(i -> i % 2 == 0)
        .distinct()
        .forEach(System.out::println);

 

스트림 슬라이싱

스트림은 요소를 일부만 선택하거나 스킵하는 기능을 제공한다.

 

Predicate을 이용한 슬라이싱

기본 filter 기능을 사용하여 320 칼로리 미만인 dish를 골라내야 하는 상황이라면

아래와 같이 구현하여 전체 요소를 필터링하게 할 수 있다.

List<Dish> specialMenu = Arrays.asList(
    new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("rice", true, 350, Dish.Type.OTHER), 
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER)
);

// 전체 요소 대상으로 칼로리가 320 이하인 dish 필터링 
List<Dish> filteredMenu 
    = specialMenu.stream()
        .filter(dish -> dish.getCalories() < 320)
        .collect(toList());

 

하지만, 만약 specialMenu 목록이 이미 칼로리로 정렬되어 있다면?

칼로리가 320 이상인 dish가 발견된 경우 반복을 중단하고 결과를 반환하는 것이 효율적이다. 

이렇게 특정 기준 발견 시점에 반복을 멈추고 이후 연산을 하지 않아도 되는 상황에서는 takeWhile, dropWhile 메서드를 사용할 수 있다.

  • takeWhile : 특정 기준점(ex. 필터 조건 발견시점) 이전 요소들을 반환. 이후 요소들은 연산하지 않음
  • dropWhile : 특정 기준점 이전 요소들을 버림. 이후 요소들은 연산하지 않고 반환
// takeWhile을 사용하여 320 보다 크거나 같은 요리가 나왔을 때 작업 중단. 이전 발견된 요소들 반환
List<Dish> sliceMenu1
    = specialMenu.stream()
        .takeWhile(dish -> dish.getCalories() < 320)
        .collect(toList());

// dropWhile을 사용하면, 320 보다 크거나 같은 요리가 나오면 작업을 중단. 남은 요소를 모두 반환
List<Dish> sliceMenu2
    = specialMenu.stream()
        .dropWhile(dish -> dish.getCalories() < 320)
        .collect(toList());

 

스트림 축소

  • limit : n 개의 요소를 반환. 만약 정렬되어 있지 않은 상태라면, 정렬되지 않은 상태에서 최대 n 개 요소 반환
// filter 결과 중 3개 요소만 반환
// filter 결과가 칼로리로 정렬되어 있지 않은 경우면, 정렬되지 않은 상태 그대로 3개 요소 반환
List<Dish> dishes = specialMenu.stream()
        .filter(dish -> dish.getCalories() > 300)
        .limit(3)
        .collect(toList());

 

요소 건너뛰기 

  • skip : 처음 n개 요소를 제외한 스트림을 반환
// 처음 2개 요소를 제외한 스트림을 반환
List<Dish> dishes = specialMenu.stream()
        .filter(dish -> dish.getCalories() > 300)
        .skip(2)
        .collect(toList());

 

매핑

map은 스트림을 인수로 받아 다른 객체로 변환된 스트림을 반환한다. 또한, map을 여러 번 연결시켜 아래처럼 사용할 수 있다.

// String -> Integer 스트림으로 변환
List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
List<Integer> wordLengths = words.stream()
        .map(String::length)
        .collect(toList());

// Dish -> String 스트림으로 변환
List<String> dishNames = menu.stream()
        .map(Dish::getName)
        .collect(toList());

// Dish -> String -> Integer 스트림으로 변환
List<Integer> dishNamesLengths = menu.stream()
        .map(Dish::getName)
        .map(String::length)
        .collect(toList());

 

스트림 평면화

map은 원하는 객체 스트림을 반환해 줄 수 있다. 

하지만, 만약 map이 반환해 줄 수 있는 스트림이 단일 객체 스트림이 아닌 배열 스트림뿐이라면? 이후 연산에 문제가 생기게 될 수 있다.

이런 경우 flatMap을 사용하여 각 배열에 있는 컨텐츠들을 모아 하나의 스트림으로 구성할 수 있다.

  • flatMap : 배열 스트림의 각 컨텐츠를 모아 하나의 스트림으로 구성. 스트림 평면화 수행
// words에서 고유 문자를 추출하고자 함
// 첫 번째 시도. map을 사용하였으나 map 결과로 String[]이 반환되어 distinct 가 각 단어 기준으로 연산됨
words.stream()
        .map(word -> word.split("")) // map은 Stream<String[]> 반환
        .distinct()
        .collect(toList());

// 두 번째 시도. 각 배열을 별도 스트림으로 생성하려 했으나 두 번째 map에서 Stream<Stream<String>>이 반환되며 문제 해결되지 못함
words.stream()
        .map(word -> word.split("")) // Stream<String[]> 반환
        .map(Arrays::stream)  		// Stream<Stream<String>> 반환
        .distinct()
        .collect(toList());

// 세 번째 시도. flatMap을 사용해 배열 스트림 각 컨텐츠를 하나의 스트림으로 구성.
List<String> uniqueCharacters = words.stream()
                .map(word -> word.split("")) 	// Stream<String[]> 반환
                .flatMap(Arrays::stream)		// Strema<String> 반환
                .distinct()
                .collect(toList());

 

검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.

관련 스트림 API로는 allMatch, anyMatch, noneMatch, findFirst, findAny 등이 있다.

 

요소 일치 검사

allMatch, anyMatch, noneMatch는 최종 연산이자 쇼트서킷 기법의 연산이다.

쇼트서킷 기법이란 자바의 &&, || 와 같이 조건에 일치 또는 불일치하는 결과가 나오면 즉시 결과를 반환하여 이후 연산을 수행하지 않는 기법을 말한다. 이는 무한한 요소를 가진 스트림도 유한한 크기로 줄일 수 있는 유용한 연산 기법이다.

  • anyMatch : 조건에 맞는 요소가 하나라도 있는지 검사
  • allMatch : 모든 요소가 조건에 맞는지 검사
  • noneMatch : 조건에 맞는 요소가 하나도 없는지 검사 (allMatch와 반대 연산)
// anyMatch로 조건에 맞는 요소가 하나라도 있는지 검사
if (menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) vegetarian friendly!");
}

// allMatch로 모든 요소가 조건에 맞는지 검사
boolean isHealthy = menu.stream()
        .allMatch(dish -> dish.getCalories() < 1000);

// noneMatch로 조건에 맞는 요소가 하나도 없는지 검사 (allMatch와 반대 연산)
boolean isHealthy = menu.stream()
        .noneMatch(dish -> dish.getCalories() >= 1000);

 

요소 검색

findAny 기법은 임의 요소를 반환하는 기법으로 최종적으로 Optional을 반환한다.

findAny : 현재 스트림에서 임의의 요소를 반환하는 최종 연산. 다른 스트림과 연결해서 사용 가능. 쇼트서킷 기법 연산

// 채식 메뉴 중 아무 것이나 반환
Optional<Dish> dish =
        menu.stream()
                .filter(Dish::isVegetarian)
                .findAny();

Optional을 값의 존재, 부재를 표현하는 컨테이너 클래스로,

findAny의 경우 반환할 값이 없을 수도 있기 때문에 Optional을 반환타입으로 가진다.

아래는 Optional이 제공하는 기능이고, 예제의 dish는 위에서 얻은 Optional<Dish> 객체이다.

// isPresent는 Optional인 dish가 값을 포함하고 있으면 true 반환, 아니면 false 반환
dish.isPresent();   

// ifPresent(Consumer<T>)는 값이 있으면 주어진 Consumer block 실행
dish.ifPresent(d -> System.out.println(d.getName()));

// get()은 Optional에 값이 있으면 해당 값을 반환. 없으면 NoSuchElementException 발생
dish.get();

// orElse()는 Optional에 값이 있으면 값을 반환. 없으면 셋팅된 기본값을 반환
dish.orElse(null);

 

첫 번째 요소 찾기

만약 스트림이 정렬되어 있는 연속 데이터라면 논리적인 아이템 순서가 정해져 있을 수 있다.

이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까? 만약 병렬 스트림이라면 기존 정렬된 순서대로 반환이 되지 않을 수 있다.

요소 반환 순서가 상관 없다면 상대적으로 제약이 적은 findAny를 쓰는게 좋지만,

만약 논리적 정렬 순서도 고려하여 첫 번째 요소를 찾는다면 findFirst 기능을 사용할 수 있다.

  • findFirst : 논리적 아이템 순서가 정해져 있는 경우, 조건에 맞는 첫 번째 요소를 반환
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5, 6);

// 숫자 리스트에서 3으로 나눠 떨어지는 첫 번째 제곱값 반환
Optional<Integer> firstSquareDivisibleByThree =
       someNumbers.stream()
               .map(n -> n * n)
               .filter(n -> n % 3 == 0)
               .findFirst(); // 9