Home 스트림 활용
Post
Cancel

스트림 활용

스트림 활용

데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다. 따라서 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다. 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다.

여러 스트림 사용법을 다루어 보겠다.


필터링 - filter()

스트림의 요소를 선택하는 방법은 Predicate를 이용한 필터링, 고유 요소 필터링 두가지 존재한다.

프레디케이트(Predicate) 필터링

1
2
3
4
/* 채식 요리만 필터링 */
List<Dish> list = menu.stream()
                      .filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드 참조
                      .collect(toList());

고유 요소 필터링

스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드를 지원한다.

1
2
3
4
5
6
/* distinct 예제
List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 2, 4);
  numbers.stream()
  .filter(i -> i % 2 == 0)
  .distinct() //중복을 필터링
  .forEach(System.out::println);


스트림 슬라이싱 - takeWhile(), dropWhile()

스트림의 요소를 선택하거나 스킵하는 다양한 방법이 있다.

프레디케이트(Predicate)를 이용한 슬라이싱

스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두가지 메서드를 지원한다.

TAKEWHILE 활용

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 정렬되어 있는 데이터*/
List<Dish> specialMenu = Arrays.asList(
    new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
    new Dish("prawns", false, 300, Dish.Type.FISH),
    new Dish("rice", true, 350, Dish.Type.OTHER),
    new Dish("chicken", false, 400, Dish.Type.MEAT),
    new Dish("french fries", true, 530, Dish.Type.OTHER)
);

List<Dish> filteredMenu
    = specialMenu.stream()
                 .filter(dish -> dish.getCalories() < 320)
                 .collect(toList());

filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용하게 된다.

Q. 위처럼 정렬되어 있는 데이터를 전체 스트림을 반복해서 처리하는게 효율적일까?

A. 아니다. 위의 경우 데이터가 적어서 상관없지만 클 경우 상당한 차이가 있다. 이때 takeWhile 를 이용하면 된다.

1
2
3
4
List<Dish> slicedMenu1
    = specialMenu.stream()
                 .takeWhile(dish -> dish.getCalories() < 320)
                 .collect(toList());

takeWhile을 활용할 경우 조건을 불만족 (칼로리가 320 보다 같거나 클 경우) 반복을 중단하고 반환한다.

DROPWHILE 활용

1
2
3
4
List<Dish> slicedMenu2
    = specialMenu.stream()
                 .dropWhile(dish -> dish.getCalories() < 320)
                 .collect(toList());

dropWhile 의 경우에는 takeWhile 과 반대로 조건이 불만족하게 되면 그 지점에서 작업을 중단하고 남은 요소를 반환한다.


스트림 축소 - limit(N)

스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(N) 메서드를 지원한다.

1
2
3
4
5
List<Dish> dishes = specialMenu
                        .stream()
                        .filter(dish -> dish.getCalories() > 300)
                        .limit(3)
                        .collect(toList());

limit(N)는 프레디케이트와 일치하는 처음 N요소를 선택한 다음 즉시 결과를 반환한다.

  • 만약 정렬되어 있지 않는 데이터라면 정렬되지 않은 상태로 반환한다.

요소 건너뛰기 - skip(N)

스트림은 처음 N개 요소를 제외한 스트림을 반환하는 skip(N)를 지원한다.

1
2
3
4
5
List<Dish> dishes = specialMenu
                        .stream()
                        .filter(dish -> dish.getCalories() > 300)
                        .limit(3)
                        .collect(toList());

skip(N) 메서드는 limit(N) 메서드와 상호 보완적인 연산을 수행한다.


맵핑

스트림은 특정 데이터를 선택하는 map, flatMap 메서드를 지원한다.

스트림의 각 요소에 함수 적용하기 - map(), flatMap()

요리의 요리명만 추출하고 싶다면?

1
2
3
List<String> dishNames = menu.stream()
                             .map(Dish::getName) //Stream<String>
                             .collect(toList());

만약 요리 이름의 길이 리스트를 반환하게 하고 싶다면?

1
2
3
4
List<Integer> dishNameLengths = menu.stream()
                                   .map(Dish::getName)
                                   .map(String::length)
                                   .collect(toList());

스트림 평면화

["Hello", "World"] 리스트가 있다면 ["H," "e," "l," "o," "W," "r," "d"] 의 리스트를 반환해보자.

1
2
3
4
words.stream()
     .map(word -> word.split("")) // Stream<String[]>
     .distinct()
     .collect(toList());

이렇게 생각할 수 있는데 문제점이 있다.

map의 반환 값이 Stream<String[]>으로 원했던 Stream<String>이 아니다.

map과 Arrays.stream 활용

1
2
3
4
5
words.stream()
     .map(word -> word.split("")) // Stream<String[]>
     .map(Arrays::stream) // Stream<Stream<String>>
     .distinct()
     .collect(toList());

이렇게 할경우 스트림 리스트List<Stream<String>> 가 만들어 지면서 문제가 해결되지 않는다.

이를 해결하기 위해서는 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 배열을 별도의 스트림으로 만들어야 한다.

flatMap 사용

flatMap은 각 배열을 스트림이 하나의 평면화된 스트림을 반환한다.

1
2
3
4
5
6
List<String> uniqueCharacters =
  words.stream()
       .map(word -> word.split(""))  // Stream<String[]>
       .flatMap(Arrays::stream)      // Stream<String>
       .distinct()
       .collect(toList());


검색과 매핑

스트림은 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리인 allMatch, anyMatch, noneMatch, findFirst, findAny 를 지원한다.

프레디케이트(Predicate)가 적어도 한 요소와 일치하는 확인 - anyMatch()

1
2
3
if (menu.stream().anyMatch(Dish::isVegetarian)) {
    System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

프레디케이트(Predicate)가 모든 요소와 일치하는지 확인 - allMatch()

1
2
boolean isHealthy = menu.stream()
                        .allMatch(dish -> dish.getCalories() < 1000);

프레디케이트(Predicate)가 모든 요소가 일치하지 않는지 확인 - noneMatch()

1
2
boolean isHealthy = menu.stream()
                        .noneMatch(d -> d.getCalories() >= 1000);

현재 스트림에서 일치하는 임의의 요소 하나 반환 - findAny()

1
2
3
4
Optional<Dish> dish =
  menu.stream()
      .filter(Dish::isVegetarian)
      .findAny();

현재 스트림에서 일치하는 첫 번째 요소 하나 반환 - findFirst()

1
2
3
4
5
6
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

Q. findFirst와 findAny 메서드 둘다 필요할까?

A. 병렬성 때문에 필요하다. 병렬 실행에서는 첫 번쨰 요소를 찾기 어렵다. 반환 순서가 상관없다면 findAny를 사용하는 것이 좋다.


리듀싱

스트림은 리듀싱 연상(모든 스트림 요소를 처리해서 값으로 도출하는)을 지원한다.

함수형 프로그래밍 언어 용어로는 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드 라고 부른다.

요소의 합

forEach 문

1
2
3
4
int sum = 0;
for(int x : numbers) {
  sum += x;
}

reduce를 이용한 어플리케이션 반복된 패턴을 추상화

1
int sum = numbers.stream().reduce(0, (a,b) -> a + b);

메서드 참조를 이용

1
int sum = numbers.stream().reduce(0, Integer::sum);

초기값을 받지 않도록 오버로드된 reduce

1
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

Q. 왜 Optional 를 반환할까?

A. 초기값이 없는 reduce는 합계를 반환할 수 없기 때문에 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.

최댓값과 최소값

reduce를 이용해서 스트림에서 최댓값, 최솟값을 찾을 수 있다.

최댓값

1
Optional<Integer> max = numbers.stream().reduce(Integer::max);

최소값

1
Optional<Integer> min = numbers.stream().reduce(Integer::min);

Q. 기존의 반복으로 합계를 구하는 방법도 있는데 reduce를 쓰는 이유는?

A. reduce 를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce 를 실행 할 수 있게 된다. 반복적인 합계에서는 sum 이라는 변수를 공유해 하므로 쉽게 병렬화 하기 어렵다.

강제적으로 동기화시키더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다.

바로, stream() 을 parallelStream() 으로 바꿔서 처리하는 방법인데, 이렇게 처리하게 된다면, 병렬성을 얻을 수 있는 대신 몇가지 제약 사항이 따르게 된다. 이는 추후에 더 자세히 설명한다

상태 없음과 상태 있음

각각의 연산은 내부적인 상태를 고려해야 한다.

  • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다. 따라서 이들은 내부 상태를 갖지 않는 연산(stateless operaion) 이다.
  • sorted, distinct 같은 연산은 이전 연산에 대한 정보를 알고 있어야 한다. 이러한 연산을 내부 상태를 갖는 연산(stateful operation) 이라 한다.

숫자형 스트림 - 기본형 특화 스트림

reduce 메서드로 스트림 요소의 합을 구하는 방법은 아래와 같다.

1
2
3
int calories = menu.stream()
                  .map(Dish::getCalories)
                  .reduce(0, Integer::sum);

위의 코드는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.

1
2
3
int calories = menu.stream()
                  .map(Dish::getCalories)
                  .sum(); // X

map 메서드가 Stream<T> 를 생성하기 때문에 sum메서드를 지원하지 않는다. 그렇기 때문에 스트림은 기본형 특화 스트림을 제공한다.

기본형 특화 스트림에는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream을 제공한다.

숫자 스트림으로 매핑

스트림을 특화 스트림으로 변환할 때 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다.

1
2
3
int calories = menu.stream()
                  .mapToInt(Dish::getCalories) // Stream<Dish>
                  .sum(); // IntStream

객체 스트림으로 복원하기

boxed 메서드를 통해 숫자 스트림을 다시 스트림으로 복원할 수 있다.

1
2
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

기본값 : OptionalInt

IntStream에서 최댓값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.

스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 어떻게 구별하기 어렵기 때문에 이전에 값이 존재하는 여부를 가르킬 수 있는 컨테이너 클래스 Optional을 이용한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전을 제공한다.

1
2
3
4
5
OptionalInt maxCalories = menu.stream()
                              .mapToInt(Dish::getCalories)
                              .max();

int max = maxCalories.orElse(1) // 값이 없을 경우 기본값을 명시적으로 설정

숫자 범위

프로그램에서는 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.

IntStream, LongStream 에서는 rangerangeClosed 라는 두 가지 정적 메서드를 제공한다.

두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.

range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면 rangeClosed 는 시작값과 종료값이 결과에 포함된다.

1
2
3
IntStream evenNumbers = IntStream.rangeClosed(1, 100) // [1,100]의 범위를 나타낸다.
  .filter(n -> n % 2 == 0); // 1 부터 100까지 짝수 스트림
System.out.println(evenNumbers.count());

스트림 만들기

값으로 스트림 만들기

1
Stream<String> stream = Stream.of("Modern", "java", "In", "Action");

빈 스트림 만들기

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

null이 될 수 있는 객체로 스트림 만들기

1
2
String homeValue = System.getProperty("home");
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(value);

배열로 스트림 만들기

1
2
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

파일로 스트림 만들기

1
2
3
4
5
6
7
8
9
10
long uniqueWords = 0;
// 스트림은 자원을 자동으로 해제할 수 있는 AutoCloseable 이므로 try-with-resources 사용
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) {}// 예외처리


함수로 무한 스트림 만들기

스트림은 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate, Stream.generate를 제공한다.

두 연산을 이용해서 무한 스트림(언바운드 스트림), 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과는 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

무한 값

iterable

1
2
3
Stream.iterate(0, n -> n + 2)
      .limit(10) // 무한 스트림을 limit 함수로 제한한다.
      .forEach(System.out::println);

Predicate

1
2
IntStream.iterate(0, n -> n < 100, n -> n + 4) // Predicate 로 제한
  .forEach(System.out::println);

generate

1
Stream.generate(Math::random)
This post is licensed under CC BY 4.0 by the author.