[Java] Stream 정리

2021. 12. 5. 16:48개발노트

Stream 특징

  • Java 8 부터 지원한다.
  • 원본데이터 자체를 변경하지 않는다.
  • 재사용이 불가능하다.
  • 내부 반복을 사용으로 코드가 간결해지고 병렬처리가 쉽다.
  • 람다 표현식을 이용해서 선언형으로 코드를 구현할 수 있다.
  • 스트림 자신을 반환해 스트림 연산 끼리 연결, 파이프 라인을 구성한다. (데이터 소스에 적용하는 데이터베이스 질의와 비슷하다.)

이전 코드와 비교

테스트를 위한 Dish 클래스

더보기
public 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;
  }

  public enum Type {
    MEAT,
    FISH,
    OTHER
  }

  @Override
  public String toString() {
    return name;
  }

  public static final List<Dish> menu = Arrays.asList(
      new Dish("pork", false, 800, Type.MEAT),
      new Dish("beef", false, 700, Type.MEAT),
      new Dish("chicken", false, 400, Type.MEAT),
      new Dish("french fries", true, 530, Type.OTHER),
      new Dish("rice", true, 350, Type.OTHER),
      new Dish("season fruit", true, 120, Type.OTHER),
      new Dish("pizza", true, 550, Type.OTHER),
      new Dish("prawns", false, 400, Type.FISH),
      new Dish("salmon", false, 450, Type.FISH)
  );

}

 

저칼로리의 요리명을 반환하고, 칼로리를 기준으로 요리를 정렬하는 코드를 구현 한다고 할 때

 

Stream 을 사용하지 않고 코드를 작성

  • 아래와 같이원하는 값을 찾기 위해 전처리하는 과정에서 코드가 길어지게 된다.
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;
  }

 

Stream으로 코드 작성

  • 선언형 코드 구현, 내부 반복으로 짧은 코드로 구현 가능하다.
public static List<String> getLowCaloricDishesNamesInJava8(List<Dish> dishes) {
    return dishes.stream()
        .filter(d -> d.getCalories() < 400) // 400 칼로리 이하의 요리 선택
        .sorted(comparing(Dish::getCalories)) // 칼로리로 요리 정렬
        .map(Dish::getName) // 요리명 추출
        .collect(toList()); // 추출된 모든 요리명을 리스트에 저장
  }

 

두 코드는 모두 아래와 같은 Output 을 가진다.

season fruit
rice

 

Stream 이란?

Stream 이란 '데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소' 로 정의된다.

  • 연속된 요소 : 컬렉션과 마찬가지로 연속된 값 집합의 인터페이스를 제공, 컬렉션은 자료구조로 시간과 공간에 관련된 요소 저장 및 연산이 주를 이룬다면 스트림은 filter, sorted, map 같은 표현 계산식이 주를 이룬다.
  • 소스 : 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소의 순서를 유지한다.
  • 데이터 처리 연산 : 스트림은 함수형 프로그래밍, 데이터베이스와 비슷한 연산을 지원 (filter, map, reduce, find, match, sort 등의 사용) 하며 순차적 또는 병렬 연산 수행이 가능하다.

예제로 확인

List<String> names = menu.stream() // 요리 리스트에서 스트림을 얻음
        .filter(dish -> dish.getCalories() > 300) // 파이프라인 연산 - 코칼로리 요리 필터링
        .map(dish -> dish.getName()) // 요리명 추출
        .limit(3) // 선착순 세 개만 선택
        .collect(toList()); // 결과를 다른 리스트로 저장

Output

[pork, beef, chicken]

 

Stream 연산

stream 생성 -> 중간 연산 -> 최종 연산 순으로 연산이 이루어진다.

 

중간 연산

filter, sorted 같은 중간 연산은 다른 스트림을 반환해 여러 중간 연산을 연결해 질의를 만들 수 있으며

중간 연산을 모두 합쳐 최종 연산으로 한 번에 처리 된다는 특징이 있다.

연산 반환 형식 연산의 인수 함수 디스크럽터 설명
filter Stream<T> Predicate<T> T -> boolean 해당 스트림에서 주어진 조건(predicate)에 맞는 요소만으로 구성된 새로운 스트림을 반환
map Stream<R> Function<T, R> T -> R 해당 스트림의 요소들을 주어진 함수에 인수로 전달하며, 그 반환 값으로 이루어진 새로운 스트림을 반환
limit Stream<T>     해당 스트림에서 전달된 개수만큼의 요소만으로 이루어진 새로은 스트림을 반환
sorted Stream<T> Comparator<T> (T, T) -> int 비교자(comparator)를 이용해 정렬하며비교자를 전달하지 않으면 영문사전 순으로 정렬
distinct Stream<T>     해당 스트림에서 중복된 요소가 제거된 새로운 스트림을 반환

 

 

최종 연산

결과를 도출하며 보통 최종 연산에 의해 List, Integer, void 등 스트림 이외의 결과가 반환된다.

연산 반환 형식 설명
forEach void 스트림의 각 요소를 소비하면서 람다를 적용
count long(generic) 스트림의 요소 개수를 반환
collect   스트림을 리듀스해 리스트, 맵, 정수 형식의 컬렉션을 만든다.