Modern Java In Action 정리

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

4장 스트림 소개

스트림(Stream)

데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

  • 자바8에 추가된 기능으로 스트림을 이용하면 선언형으로 코드를 구현하여 컬렉션 데이터를 처리할 수 있다. 선언형으로 구현한다는 것은 for 루프나 if 조건문 등의 제어 블록을 사용하지 않고 동작을 지정하는 것이다.
    → 실제 구현은 신경쓰지 않고 사용하는 SQL를 생각하자.
  • 스트림에서 제공하는 filter, sorted, map, collect 같은 메서드들은 특정 스레딩 모델에 제한되지 않고 스레드와 락을 걱정할 필요없이 편리하게 데이터 병렬처리를 가능하게 해준다.

스트림 기본 구현

  • 기본구현에 사용할 Dish 클래스
    class Dish{
      private final String name;
      private final boolean vegetarian;
      private final int calories;
      private final Type type;
    
      public Dish(String name, boolean vegetarian, int calories, Type type) {
          this.name = name;
          this.vegetarian = vegetarian;
          this.calories = calories;
          this.type = type;
      }
    
      public String getName() {
          return name;
      }
    
      public boolean isVegetarian() {
          return vegetarian;
      }
    
      public int getCalories() {
          return calories;
      }
    
      public Type getType() {
          return type;
      }
    
      enum Type {
          MEAT, FISH, OTHER
      }
    }
    
  • 스트림 구현에 사용할 Dish 컬렉션
    List<Dish> menu = Arrays.asList(
      new Dish("pork", false, 800, Dish.Type.MEAT),
      new Dish("beef", false, 700, Dish.Type.MEAT),
      new Dish("chicken", false, 400, Dish.Type.MEAT),
      new Dish("french fries", true, 530, Dish.Type.OTHER),
      new Dish("salmon", false, 450, Dish.Type.FISH)
    );
    
  • 스트림 기본구현
    @Test
    public void 스트림_기본구현(){
      List<String> threeHighCaloricDishNames =
          menu.stream() // 컬렉션에서 스트림(Stream<Dish>)을 가져온다.
              .filter(dish -> dish.getCalories() > 300) // 해당 조건의 요소만 추출한다.
              .map(Dish::getName) // 이름(String) 속성을 스트림(Stream<String)으로 가져온다.
              .limit(3) // 3개를 제외하고 truncate한다.
              .collect(Collectors.toList()); // 스트림을 컬렉션(리스트)로 변환한다.
    
      System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken]
    }
    
  • filter, map 메서드는 인자로 Functional Interface 인스턴스를 받기 때문에 람다표현식이나, 메서드참조로 간결하게 코딩이 가능하다. 또한 메서드들은 실행 결과로 스트림을 리턴하기 때문에 파이프라인 형태가 된다.
    → 빌더 패턴과 유사하다.
  • 서로 연결이 가능한 filter, map, limit는 중간연산이며, collect는 스트림을 닫는 최종연산이다.

스트림과 컬렉션

  • 컬렉션은 DVD에 저장된 영화에 비유할 수 있다. 모든 데이터(영화 내용 전부)가 메모리(DVD)에 저장되어 있다.
    컬렉션에는 계산된 결과물이 저장되어 있으며, 주 관심사는 특정요소에 접근하여 값을 계산하거나 치환한다.
  • 스트림은 인터넷으로 스트리밍하는 영화에 비유할 수 있다.
    영화 전체를 모두 받는 것이 아니라 미리 몇 프레임만 내려받아 재생이 가능할 수 있다. 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조다.
  • 스트림은 한번만 탐색이 가능하다.
    이미 소비된 스트림을 사용하려 하면 Exception이 발생하므로 다시 생성하여 사용해야 한다.
  • 컬렉션은 데이터를 순회하기 위해서는 루프문을 이용해 명시적으로 반복해야 한다. → 외부반복
  • 스트림은 반복을 알아서 처리(중간값 저장, 최적화, 병렬성 구현 등) 한다. → 내부반복
    @Test
    public void 스트림_내부반복(){
      // forEach 메서드에서 반복되며 명시적으로 루프문이 필요없다.
      menu.stream()
          .forEach(dish -> System.out.println(dish.getName()));
    }
    

스트림은 게으른(lazy) 연산을 지원

  • 스트림 처리시 한 요소는 연결된 모든 파이프라인을 타고나서 다음 요소가 처리된다.
    @Test
    public void 스트림_게으른_연산(){
      List<String> threeHighCaloricDishNames =
          menu.stream()
              .filter((dish) -> {
                  System.out.println("filtering :: " + dish.getName());
                  return dish.getCalories() > 300;
              })
              .map((dish) -> {
                  System.out.println("mapping :: " + dish.getName());
                  return dish.getName();
              })
              .limit(3)
              .collect(Collectors.toList());
    
      System.out.println(threeHighCaloricDishNames); // [pork, beef, chicken]
    }
    
  • 콘솔출력으로 스트림 처리 순서를 보면 한 요소씩 filter → map → limit → collect 파이프라인을 타는것을 볼 수 있다. 중간 연산들(filter → map → limit)을 합친 다음에 최종 연산(collect)에서 한 번에 처리한다.
    filtering :: pork
    mapping :: pork
    filtering :: beef
    mapping :: beef
    filtering :: chicken
    mapping :: chicken
    [pork, beef, chicken]