Modern Java In Action 정리

Modern Java In Action을 읽고 내용을 정리해본다.

5장 스트림 활용

필터링

  • filter() 메서드는 Predicate<T>를 인자로 일치하는 모든 요소를 포함하는 스트림을 반환한다.
    @Test
    public void 스트림_filter(){
      List<Integer> numbers = Arrays.asList(1, 2, 3, 1, 2, 4);
      numbers.stream()
             .filter(i -> i % 2 == 0) // 짝수만 필터링
             .distinct() // 중복요소 제거, hashCode와 equals로 결정된다.
             .forEach(System.out::println); // 출력
    }
    
  • distinct(), skip(n), limit(n) 와 같이 사용되어 스트림을 축소할 수 있다. 직관적으로 동작이 메서드명에 나타나므로 따로 정리를 하지 않는다.
  • 자바9에서 추가된 takeWhile(), dropWhile() 를 활용하면 기본 filter에서 추가 동작을 지정할 수 있다.
    (이미 정렬된 상태에서 유용하게 적용이 가능하다.)
@Test
public void 스트림_takeWhile_dropWhile(){
    // 메뉴 컬렉션을 칼로리를 기준으로 오른차순 정렬
    menu.sort(Comparator.comparing(Dish::getCalories));
    menu.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));

    // takeWhile() : 조건에 만족할 때까지 스트림을 반환, false시 반복 중단
    List<Dish> lowCalDish = menu.stream()
            .takeWhile(dish -> dish.getCalories() < 450)
            .collect(Collectors.toList());

    System.out.println("\n450 칼로리 미만 ------");
    lowCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));

    // dropWhile() : 조건에 만족하는 요소까지 버림, true부터 스트림을 반환
    List<Dish> highCalDish = menu.stream()
            .dropWhile(dish -> dish.getCalories() < 450)
            .collect(Collectors.toList());

    System.out.println("\n450 칼로리 이상 ------");
    highCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));
}
chicken(400) salmon(450) french fries(530) beef(700) pork(800) 
450 칼로리 미만 ------
chicken(400) 
450 칼로리 이상 ------
salmon(450) french fries(530) beef(700) pork(800) 

매핑

  • 스트림API는 스트림의 요소에서 다른 요소로 변환할 수 있는 map()과 flatMap() 메서드를 제공한다.
    <R> Stream<R> map(Function<? super T, ? extends R> var1);
    <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> var1);
  • 매핑 메서드들은 스트림의 한 요소(T 타입)에서 맵핑되는 새로운 타입(R 타입)의 요소를 만들어 반환한다.
  • 아래 예시에서는 map(Function<String, Integer>) 를 이용하여 Integer를 요소로 갖는 스트림을 생성하고 이를 List로 반환했다.
@Test
public void map_테스트(){
    List<String> strs = List.of("하이", "헬로우");

    // 문자열 길이를 요소로 같는 리스트 생성
    List<Integer> lengths = strs.stream()
            .map(String::length)
            .collect(Collectors.toList());
}
  • map()은 스트림의 요소별로 1대1로 반환하기 때문에 이해가 쉽지만 flatMap()은 다른 동작을 한다.
  • map()과 flatMap()에 파라미터로 주는 Function 인터페이스를 보면 반환형이 다른것을 볼 수 있다.
    • map() : Function<? super T, ? extends R>
    • flatMap() : Function<? super T, ? extends Stream<? extends R>>
  • flatMap의 파라미터인 Function 인터페이스의 구현체는 반환하는 객체의 타입이 Stream 타입이고, flatMap은 Stream 그 자체가 아닌 Stream의 컨텐츠로 매핑한다. 즉, map과 다르게 flatMap은 하나의 평면화된 스트림을 반환한다.
  • 차이를 볼 수 있게 예제 코드를 확인한다.
    문자열 리스트에서 중복을 제거한 한글자가 요소인 리스트를 구한다.
    @Test
     public void flatMap_테스트(){
       List<String> strs = List.of("HI", "HELLO");
     
       List<String> distinctStrs = strs.stream()
               .map(s -> s.split("")) // Stream<String[]>
     //          .map(sArr -> Arrays.stream(sArr)) // Stream<Stream<String>>
               .flatMap(sArr -> Arrays.stream(sArr)) // Stream<String>
               .distinct()
               .collect(Collectors.toList());
     
       distinctStrs.forEach(System.out::println); // H I E L O
     }
    

sArr -> Arrays.stream(sArr) 의 실행결과인 Stream 에 대해

  • map은 Stream<Stream>을 리턴

ModernJava3_0

  • flatMap은 Stream의 컨텐츠인 String으로 하나의 평면화면 Stream을 리턴했다. distinct()도 의도한 대로 글자당 중복을 제거하도록 동작한다.

ModernJava3_1

  • 이 밖에도 flatMap은 아래와 같이 변환을 지원하므로 자세한 내용은 Mkyong flatMap Example 의 내용을 참고하자
    Stream<String[]>        -> flatMap ->   Stream<String>
    Stream<Set<String>>	    -> flatMap ->   Stream<String>
    Stream<List<String>>    -> flatMap ->   Stream<String>
    Stream<List<Object>>    -> flatMap ->   Stream<Object>
    

검색과 매칭

  • 스트림API는 특정 속성이 데이터 집합에 있는지 여부를 검색하는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
@Test
public void 검색과_매칭(){
    List<String> strs = List.of("HI", "HELLO");

    boolean isAllStartWithH = strs.stream()
                                  .allMatch(s -> s.startsWith("H"));
    System.out.println("모두 H로 시작합니다. : " + isAllStartWithH);

    boolean isOneEndWithO = strs.stream()
                                .anyMatch(s -> s.endsWith("O"));
    System.out.println("한 요소 이상이 O로 끝납니다. : " + isOneEndWithO);

    boolean isNoneEndWithK = strs.stream()
                                 .noneMatch(s -> s.endsWith("K"));
    System.out.println("모두 K로 끝나지 않습니다. : " + isNoneEndWithK);

    Optional<String> first = strs.stream().findFirst();
    System.out.println("첫번째 요소는(First) : " + first.get());


    Optional<String> any = strs.stream().findAny();
    System.out.println("첫번째 요소는(Any) : " + any.get());
}
  • findFirst와 findAny가 모두 필요한 이유는 병렬성 때문이다. 요소의 반환순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

리듀싱

  • 최종 결과값이 나올 때까지 스트림의 요소를 반복적으로 처리하는 연산을 리듀싱 연산이라고 한다.
  • 리듀싱 연산은 reduce 메서드로 수행할 수 있으며 사용은 예제코드를 참고한다.
    @Test
    public void reduce(){
      int[] nums = new int[]{1, 2, 3, 4, 5};
    
      // 초기값을 사용하여 reduce 연산
      int sumWithInitVal = Arrays.stream(nums)
                                 .reduce(0, Integer::sum);
      System.out.println("합계 : " + sumWithInitVal); // 15
    
      // 초기값 없이 reduce 연산
      OptionalInt optSum = Arrays.stream(nums)
                                 .reduce(Integer::sum);
      System.out.println("합계 : " + optSum.getAsInt()); // 15
    
      // 초기값이 없기 때문에 reduce 연산시 결과값이 없을 수 있다.
      OptionalInt optSum2 = Arrays.stream(new int[]{})
                                  .reduce(Integer::sum);
      System.out.println("합계 : " + optSum2.orElse(0)); // 0
    }
    

    스트림API에서 지원하는 연산에 대한 정리

    ModernJava3_2