Java

스트림(2) - Stream API의 다양한 연산

뽀루피 2024. 10. 26. 17:20

스트림 API가 지원하는 다양한 연산에 대해서 알아보자.

 

요약

 

 

필터링

.filter()

filter 메소드는 predicate(boolean을 반환하는 함수)를 인수로 받아서 true를 반환하는 모든 요소를 포함하는 스트림을 반환한다.

List<Dish> vegetarianMenu = menu.stream()
                                .filter(Dish::isVegetarian)
                                .collect(toList());
// 채식요리

 

.distinct()

스트림 요소의 중복을 제거하는 distinct 메소드도 지원한다.(중복 여부는 equals와 hashCode로 결정된다)

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

 

 

 

정렬시 스트림 슬라이싱

.takeWhile()

스트림 요소를 쌓다가 해당 predicate에 false인 요소가 나오면 연산을 중단하고 true인 곳까지의 스트림을 반환한다.

List<Dish> slicedMenu1 = specialMenu.stream()
                                    .dropWhile(dish -> dish.getCalories() < 320)
                                    .collect(toList());
// 칼로리 320 미만 요리만

 

.dropWhile()

takeWhile 메소드의 반대로 작용하는 메소드. predicate가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다. predicate가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다.

List<Dish> slicedMenu2 = specialMenu.stream()
                                    .dropWhile(dish -> dish.getCalories() < 320)
                                    .collect(toList());
// takeWhile의 반대로 반환

 

 

 

스트림 축소

.limit()

지정한만큼 최대 요소 n개를 반환할 수 있다. 최대 요소에 못 미친다면 못 미친만큼 반환한다.

List<Dish> dishes = specialMenu.stream()
                               .filter(dish -> dish.getCalories() > 300)
                               .limit(3)
                               .collect(toList());
// 필터링 뒤 요소 3개만 반환

 

 

 

요소 건너뛰기

.skip()

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

List<Dish> dishes = menu.stream()
                        .filter(d -> d.getCalories() > 300)
                        .skip(2)
                        .collect(toList());
// 필터링한 뒤 요소 2개를 제외한 나머지 반환

 

 

 

매핑 mapping

.map()

함수를 인수로 받는 메소드이며 함수가 적용된 결과가 새로운 요소로 매핑된다(새로운 버전의 스트림을 만든다).

List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
List<Integer> wordLengths = words.stream()
                                 .map(String::length)
                                 .collect(toList());
// 각 요소의 길이로 치환해서 스트림 반환

 

.flatMap()

각 배열을 스트림이 아니라 평면화된 스트림으로 반환한다. 이해하기 어려우니 예시로 설명하겠다.

예를들어, ["Hello", "World"] 리스트가 있다면 ["H", "e", "l", "o", "W", "r", "d"]처럼 고유 문자로 이루어진 리스트를 반환하고 싶다고 가정하자.

words.stream()
     .map(word -> word.split(""))
     .distinct()
     .collect(toList());
// List<String[]> 반환

 

map 메소드를 통해서 요구사항을 만들면 String[]가 Stream에 등록되어 mapping이 진행되고 결국 map이 반환하는 값은 Stream<String[]>이 된다. flatMap을 사용하면 다음처럼 문제를 해결할 수 있다.

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

 

flatMap 메소드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.

String[] words = {"hello", "world"};

// map() 진행

Stream<String[]> -> { {"h", "e", "l", "l", "o"}, {"w", "o", "r", "l", "d"} }

// flatMap() 진행

Stream<String> -> {"h", "e", "l", "l", "o", "w", "o", "r", "l", "d"}

// distinct() 진행

{"h", "e", "l", "o", "w", "r", "d"}

 

 

 

검색과 매칭

.anyMatch()

스트림의 요소 중 하나라도 주어진 Predicate 조건에 만족하는 요소가 있으면 true를 반환한다.

if(menu.stream().anyMatch(Dish::isVegetarian)) { 
    System.out.println("채식 요리가 있습니다."); 
} // 채식 요리가 있는지 확인

 

.allMatch()

모든 요소가 주어진 Predicate 조건을 만족하면 true를 반환한다.

boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000); 
System.out.println(isHealthy); // 모든 요리가 1000칼로리 미만인지 확인

 

.noneMatch()

모든 요소가 주어진 Predicate 조건에 만족하지 않으면 true를 반환한다.

boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000); 
System.out.println(isHealthy); // 1000칼로리 이상의 요리가 없는지 확인

 

.findAny()

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

Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny(); 
dish.ifPresent(d -> System.out.println(d.getName())); // 채식 요리 중 아무거나 하나 찾기

 

.findFirst()

스트림에서 첫 번째 요소를 반환한다. 순차적으로 처리할 때 사용하며 결과의 순서가 중요한 경우에 사용된다.

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5); 
Optional<Integer> firstSquareDivisibleByThree 
                              = someNumbers.stream()
                                           .map(x -> x * x) 
                                           .filter(x -> x % 3 == 0) 
                                           .findFirst(); 
firstSquareDivisibleByThree.ifPresent(System.out::println); 
// 제곱해서 3으로 나누어떨어지는 첫 번째 요소 찾기

 

 

 

리듀싱 reducing

.reduce()

스트림의 모든 요소를 결합해 하나의 값으로 줄여준다. 초기값을 지정하거나 지정하지 않는 두 가지 형태가 있다. 리듀싱 연산은 스트림의 각 요소를 순회하며 누적기를 통해 값을 계산하고 반환한다.

 

 

 

초기값이 있는 reduce

초기값과 BinaryOperator를 받아 스트림을 순회하며 누적 연산을 수행한다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .reduce(0, Integer::sum);
System.out.println(sum); 
// 모든 요소의 합 계산

 

 

 

초기값이 없는 reduce

초기값 없이 BinaryOperator만을 받아 Optional을 반환한다. 요소가 없을 경우 빈 Optional을 반환하므로 빈 경우에 대한 처리가 필요하다.

Optional<Integer> sum = numbers.stream()
                               .reduce(Integer::sum);
sum.ifPresent(System.out::println); 
// 모든 요소의 합을 계산하되, 요소가 없을 경우 대비

 

 

 

최대값, 최소값 찾기

reduce 메소드를 이용하여 최대값이나 최소값을 구할 수 있다.

Optional<Integer> max = numbers.stream()
                               .reduce(Integer::max);
max.ifPresent(System.out::println); 
// 최대값 구하기

Optional<Integer> min = numbers.stream()
                               .reduce(Integer::min);
min.ifPresent(System.out::println); 
// 최소값 구하기

 

 

 

.collect()

스트림의 요소들을 변환해서 결과를 다양한 자료구조로 누적하는 데 사용된다. Collector 인터페이스를 통해 스트림 요소들을 리스트, 집합, 맵 등으로 변환할 수 있다.

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> numberList = numbers.stream()
                                  .collect(Collectors.toList());
System.out.println(numberList); 
// 스트림의 모든 요소를 리스트로 수집

 

 

 

특정 속성을 기준으로 수집

collect를 이용해 스트림의 요소들을 특정 속성값에 따라 그룹화할 수 있다.

Map<Dish.Type, List<Dish>> dishesByType = menu.stream()
                                              .collect(Collectors.groupingBy(Dish::getType));
System.out.println(dishesByType);
// 요리 종류별로 그룹화

리듀싱 메소드는 스트림의 각 요소를 하나의 값으로 결합하거나 다양한 자료구조로 변환하여 누적하는 기능을 수행한다. reducecollect는 리듀싱 작업에서 중요한 역할을 하며, 각각 간단한 연산부터 복잡한 누적까지 활용할 수 있다.