Application/JAVA & Kotlin

[모던 자바 인 액션 정리] 4장. 스트림 소개

반응형

스트림


  • 자바8에서 추가된 기능으로, 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.
  • 선언형으로 구현함으로써, if 등의 제어 블록을 사용해서 어떻게 동작을 구현할지 지정할 필요 없이, 동작을 지정할 수 있다.
  • 스트림을 이용하면 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다. (parallelStream())

예시

// 기존의 코드
public static List<String> getLowCaloricDishesNamesInJava7(List<Dish> dishes) {
    List<Dish> lowCaloricDishes = new ArrayList<>();
    for (Dish d : dishes) {
      if (d.getCalories() < 400) {
        lowCaloricDishes.add(d);
      }
    }
    List<String> lowCaloricDishesName = new ArrayList<>();
    Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
      @Override
      public int compare(Dish d1, Dish d2) {
        return Integer.compare(d1.getCalories(), d2.getCalories());
      }
    });
    for (Dish d : lowCaloricDishes) {
      lowCaloricDishesName.add(d.getName());
    }
    return lowCaloricDishesName;
  }
  
  
  // 스트림을 사용한 코드
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());
  }

 

스트림의 특징


1. 파이프라이닝

대부분의 스트림 연산은 스트림 자신을 반환함으로써 스트림 연산끼리 연결해서 파이프라인을 구성할 수 있다.

Stream<T> filter(Predicate<? super T> predicate);

// 스트림 자신을 반환함으로써 아래와 같이 파이프라인을 구성할 수 있음.
 return dishes.stream()
        .filter(d -> d.getCalories() < 400)
        .sorted(comparing(Dish::getCalories))
        .map(Dish::getName)
        .collect(toList());

 


2. 딱 한번만 탐색할 수 있다

탐색된 스트림의 요소는 소비된다.

List<String> names = Arrays.asList("Java8", "Lambdas", "In", "Action");
    Stream<String> s = names.stream();
    s.forEach(System.out::println); // 탐색된 스트림의 요소는 소비된다.

    s.forEach(System.out::println); // 따라서 IllegalStateException 발생!

 

3. 내부 반복

반복자를 이용해서 명시적으로 반복하는 컬렉션과 다르게 스트림은 내부 반복을 지원한다.

 

외부 반복 vs 내부 반복

  • 외부 반복
    • for-each 등 사용자가 직접 요소를 반복해야함.
List<String> names = new ArrayList();
for (Dish dish : menu) { // 명시적으로 순차 반복함 (외부 반복)
	names.add(dish.getName());
}

 

  • 내부반복
    • 반복을 내부적으로 처리하고 결과 스트림값을 어딘가에 저장
menu.stream()
	.map(Dish::getName)
	.collect(toList());  // 내부 반복

 

내부 반복으로써 얻는 이점?

  • 작업을 투명하게 병렬로 처리하거나, 더 최적화된 다양한 순서로 처리할 수 있다. (외부 반복으로 처리한다면 최적화를 달성하기 어려움)
  • 스트림 라이브러리의 내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택함. (반면 외부반복에서는 병렬성을 스스로 관리해야함)

 

스트림 연산


스트림은 1개 이상의 중간 연산과 1개의 최종 연산으로 이루어져 있으며, Lazy한 특성을 갖는다.

  • 중간 연산: 파이프라인을 형성할 수 있는 스트림 연산(연결할 수 있는)
    • filter, map, limit, sorted, distinct ...
    • 다른 스트림을 반환한다. 이로써 여러 중간 연산을 연결해서 질의를 만들 수 있음.
    • 단말 연산을 스트림 파이프라인에 실행하기 전까지 아무 연산도 수행하지 않음 (lazy)
    • 스트림의 lazy한 특 때문에 최적화 효과를 얻을 수 있다. (쇼트 서킷 기법, 루프 퓨전)
  • 최종 연산: 스트림을 닫는 연산
    • collect, forEach, count ...
    • 스트림 파이프라인에서 결과를 도출한다.

 

 

 

반응형