본문 바로가기

프로그래밍/Java & Spring Framework

스트림(Stream) 활용

반응형

스트림을 활용하면 외부 반복내부 반복 으로 바꿀 수 있어 좀 더 직관적인 코드를 작성할 수 있습니다. 또한 병렬로 수행할지 여부를 결정할 수 있습니다.

본문에서는 스트림 API를 활용하는 방법을 정리합니다.

필터링과 슬라이싱

  • filter()
  • distinct()
  • limit()
  • skip()
// ** 프레디케이트로 필터링 **
// 스트림 인터페이스의 filter 메서드는 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);    // 2, 4 출력

// ** 스트림 축소 **
// (정렬 여부와 상관없이)지정된 개수 이하의 크기만 반환하는 limit(n) 메서드
List<Dish> dishes = menu.stream()
                        .filter(d -> d.getCalories() > 300)
                        .limit(3)
                        .collect(toList());

// ** 요소 건너뛰기 **
// 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드
List<Dish> dishes = menu.stream()
                        .filter(d -> d.getCalories() > 300)
                        .skip(2)
                        .collect(toList());

매핑

  • map()
  • flatMap() : 각 배열을 스트림이 아니라 스트림의 컨텐츠로 매핑한다.
// ** 스트림의 각 요소에 함수 적용하기 **
// 예제 1> 메뉴의 이름으로 변환(매핑)
List<String> dishNames = menu.stream()
                             .map(Dish::getName)
                             .collect(toList());
// 예제 2> 단어의 길이로 변환(매핑)
List<String> words = Arrays.asList("Java8", "Lambdas", "In", "Action");
List<Integer> wordLengths = words.stream()
                                 .map(String::length)
                                 .collect(toList());
// 예제 3> 메뉴 이름의 글자수로 변환(매핑)
List<Integer> dishNameLengths = menu.stream()
                                    .map(Dish::getName)
                                    .map(String::length)
                                    .collect(toList());

// ** 스트림 평면화 **
// ["Hello", "World"] --> ["H", "e", "l", "o", "W", "r", "d"] 로 변환 예제
List<String> words = Arrays.asList("Hello", "World");
List<String> flattedDistinctedWord = words.stream()
                                          .map(word -> word.split(""))
                                          .flatMap(Arrays::stream)
                                          .distinct()
                                          .collect(Copllectors.toList());

검색과 매칭

  • allMatch()
  • anyMatch()
  • noneMatch()
  • findFirst()
  • findAny() : 현재 스트림에서 임의의 요소를 반환. 병렬 스트림에서는 첫 번째 요소를 찾기 어렵기 때문에 제약이 적은 findAny를 사용한다.

allMatch(), anyMatch(), noneMatch() 메서드는 자바의 &&, || 연산 처럼 활용된다.

// ** Predicate가 적어도 한 요소와 일치하는지 확인 **
// 메뉴 중 채식주의자 용 메뉴가 하나라도 존재하면 메세지 출력
if (menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

// ** Predicate가 모든 요소와 일치하는지 검사 **
// 모든 메뉴가 1000 칼로리 미만인지 확인
boolean isHealthy = menu.stream()
                        .allMatch(d -> d.getCalories() < 1000);
boolean isHealthy = menu.stream()
                        .noneMatch(d -> d.getCalories() >= 1000);

// ** 임의의 요소 검색 **
// 채식 요리 선택
Optional<Dish> dish = menu.stream()
                          .filter(Dish::isVegetarian)
                          .findAny()
                          .ifPresent(d -> System.out.println(d.getName()));

// ** 첫 번째 요소 검색 **
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
                                                           .map(n -> n * n)
                                                           .filter(n -> n % 3 == 0)
                                                           .findFirst();    // 9

리듀싱

  • reduce()

reduce 메서드는 병렬 처리가 가능하다는 장점이 있다.

// ** 요소의 합 **
// before
int sum = 0;
for (int x : numbers) {
    sum += x;
}
// after
int sum = numbers.steam().reduce(0, (a, b) -> a + b);
int sum = numbers.steam().reduce(0, Integer:sum);    // 메서드 레퍼런스 사용
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));    // 초기값을 받지 않는 방법 (Optional 객체로 반환)

// ** 최대값과 최소값 **
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
연산 형식 반환 형식 사용된 함수형 인터페이스 형식 함수 디스크립터
filter 중간 연산 Stream Predicate T -> boolean
distinct 중간 연산
(상태 있는 언바운드)
Stream    
skip 중간 연산
(상태 있는 언바운드)
Stream Long  
limit 중간 연산
(상태 있는 언바운드)
Stream Long  
map 중간 연산 Stream Function<T, R> T -> R
flatMap 중간 연산 Stream Function<T, Stream> T -> Stream
sorted 중간 연산
(상태 있는 언바운드)
Stream Comparator (T, T) -> int
anyMatch 최종 연산 boolean Predicate T -> boolean
noneMatch 최종 연산 boolean Predicate T -> boolean
allMatch 최종 연산 boolean Predicate T -> boolean
findAny 최종 연산 Optional    
findFirst 최종 연산 Optional    
forEach 최종 연산 void Consumer T -> void
Collect 최종 연산 R Collector<T, A, R>  
Reduce 최종 연산
(상태 있는 언바운드)
Optional BinaryOperator (T, T) -> T
count 최종 연산 long    

숫자형 스트림

기본형 특화 스트림

Java8에는 박싱 비용을 줄이고 sum, max와 같이 자주 사용되는 숫자 관련 리듀싱 연산 메서드를 함께 제공하는 세 가지 기본형 특화 스트림을 제공한다.

  • IntStream
  • DoubleStream
  • LongStream
  • OptionalInt
  • OptionalDouble
  • OptionalLong
// ** 숫자 스트림으로 매핑 **
// mapToInt 메서드는 IntStream 인터페이스로 반환하며, 
// 기본적으로 sum, min, average 등 다양한 유틸리티 메서드를 제공한다.
int calories = menu.stream()
                   .mapToInt(Dish::getCalories)
                   .sum();

// **객체 스트림으로 복원 **
// boxed() 메서드를 사용하여 특화 스트림을 일반 스트림으로 변환할 수 있다.
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

// Optional 컨테이너 클래스
OptionalInt maxCalories = menu.stream()
                              .mapToInt(Dish::getCalories)
                              .max();
int max = maxCalories.orElse(1);    // 값이 없을 때 기본 최대값을 명시적으로 설정

숫자 범위

IntStream과 LongStream은 특정 범위의 숫자를 이용하기 위한 2가지 정적 메서드를 제공한다. :

  • range(시작값, 종료값) : 결과에 시작값, 종료값 미포함
  • rangeClosed(시작값, 종료값) : 결과에 시작값, 종료값 포함
IntStream evenNumbers = IntStream.rangeClosed(1, 100)    // 1~100
                                 .filter(n -> n % 2 == 0);    // 짝수 필터링
System.out.println(evenNumbers.count());    // 50개

스트림 만들기

값으로 스트림 만들기

  • Stream.of() : 임의의 수로 인수를 받아 스트림으로 만든다.
  • Stream.empty() : 스트림을 비운다.
Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String.toUpperCase).forEach(System.out::println);

Stream<String> emptyStream = Stream.empty();

배열로 스트림 만들기

  • Arrays.stream()
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();    // 합계 41

파일로 스트림 만들기

NIO API(비블록 I/O)도 스트림 API를 활용할 수 있게 되었다.
java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다. (ex> Files.lines)

// 파일에서 고유한 단어 수를 찾는 프로그램
// Files.lines 메서드는 주어진 파일의 행 스트림을 문자열로 반환한다.
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
    uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
                       .distinct()
                       .count();
} catch (IOException e) {

}

함수로 무한 스트림 만들기

스트림 API는 함수에서 스트림을 만들 수 있는 두 개의 정적 메서드를 제공한다.
이전에 봤던 크기가 고정된 컬렉션과는 달리 크기가 고정되지 않은 무한 스트림(Infinite Stream) 을 만들 수 있다. (언바운드 스트림(Unbounded Stream))
따라서 반드시 limit(n) 함수와 함께 사용해야 한다.

  • Stream.iterate()
    • 초기값과 람다를 인수로 받아서 새로운 값을 끊임없이 생산한다.
    • 일반적으로 연속된 일련의 값을 만들 때 사용한다.
  • Stream.generate()
    • Supplier를 인수로 받아서 새로운 값을 생산한다.
// iterate
Stream.iterate(0, n -> n + 2)
      .limit(10)
      .forEach(System.out::println);    // 0, 2, 4, 6, 8, 10, 12, 14, 16, 18 출력

// generate
Stream.generate(Math::random)
      .limit(5)
      .forEach(System.out::println);
반응형