스트림 연산 Part.1

스트림 만들기


// Stream.of
Stream<String> stream = Stream.of("Java 8", "Lambdas", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

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

  • Arrays.stream()은 배열을 인수로 받아서 스트림을 생성합니다.

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

  • java.nio.file.Files에 많은 정적 메서드가 포함되어 있기 때문에 파일과 관련된 스트림은 손쉽게 생성할 수 있습니다.

long uniqueWords = Files.lines(Paths.get(ClassLoader.getSystemResource("chap05/data.txt").toURI()), Charset.defaultCharset())
  .flatMap(line -> Arrays.stream(line.split(" ")))
  .distinct()
  .count();

System.out.println("There are " + uniqueWords + " unique words in data.txt");


// Stream.iterate
Stream.iterate(0, n -> n + 2)
      .limit(10)
      .forEach(System.out::println);

// fibonnaci with iterate
Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
      .limit(10)
      .forEach(t -> System.out.println("(" + t[0] + ", " + t[1] + ")"));

Stream.iterate(new int[]{0, 1}, t -> new int[]{t[1], t[0] + t[1]})
      .limit(10)
      .map(t -> t[0])
      .forEach(System.out::println);

// random stream of doubles with Stream.generate
Stream.generate(Math::random)
      .limit(10)
      .forEach(System.out::println);

// stream of 1s with Stream.generate
IntStream.generate(() -> 1)
      .limit(5)
      .forEach(System.out::println);

IntStream.generate(new IntSupplier() {
  public int getAsInt() {
    return 2;
  }
}).limit(5).forEach(System.out::println);

// Fibonacci Numbers
IntSupplier fib = new IntSupplier() {
  private int previous = 0;
  private int current = 1;

  public int getAsInt() {
    int nextValue = this.previous + this.current;
    this.previous = this.current;
    this.current = nextValue;
    return this.previous;
  }
};

IntStream.generate(fib).limit(10).forEach(System.out::println);

필터링

스트림 인터페이스는 Predicate<? super T> predicate를 인수로 받아서 Stream<T>를 반환하는 filter()를 지원합니다. filter() 메서드는 프레디케이트를 인수로 전달 받기 때문에 프레디케이트(Predicate)와 일치하는 모든 요소를 포함하는 Stream<T>를 반환합니다. 앞서 설명했듯이 프레디케이트는 <T>를 매개변수로 받아서 boolean을 반환하는 함수형 인터페이스 입니다.

  • distinct()는 스트림의 고유 원소로 이루어진 Stream<T>를 반환하는 것으로 중복을 허용하지 않음
  • limit(long maxSize)은 주어진 사이즈 이하의 크기를 갖는 새로운 Stream<T>를 반환
  • skip(long n)은 n개 요소를 제외한 Stream<T>를 반환

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0)
        .distinct()
        .forEach(System.out::println);

List<Dish> dishesLimit3 = menu.stream()
        .filter(d -> d.getCalories() > 300)
        .limit(3)
        .collect(toList());

dishesLimit3.forEach(System.out::println);

매핑

스트림 API는 함수를 인수로 사용하는 map()flatMap()을 지원합니다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑됩니다. 이 과정은 기존의 값을 '고친다'라는 개념보다는 '새로운 버전을 만든다'라는 개념에 가깝기 때문에 '매핑'이라는 단어를 사용합니다.

  • map()은 단일 스트림의 원소에 함수를 적용하여 스트림으로 반환하는 중간 연산임
  • flatMap() 스트림의 각각의 원소들을 다른 객체들의 스트림으로 변환하며 각각의 객체들은 아무것도 없거나 한 개 또는 그 이상의 스트림으로 변환(Function<? super T,? extends Stream<? extends R>>)하고, 이런 스트림의 내용물은 flatMap 연산으로 반환될 스트림 내부에 포함되게 됨

List<String> words = Arrays.asList("Hello", "World");
words.stream().map(word -> word.split("")).distinct().collect(toList());

위의 예제에서 map을 적용한 결과는 Stream<String[]> 입니다. 만약 해당 문자열에서 중복된 값을 제거하고 싶다면 배열을 한 번더 처리해야 합니다. 만약 map을 적용한 결과가 Stream<String>으로 반환된다면, 손쉽게 처리할 수 있습니다. flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 제공합니다.


words.stream().flatMap((String line) -> Arrays.stream(line.split("")))
  .distinct()
  .forEach(System.out::println);

검색과 매칭

특정 속성이 데이터 집합에 있는 여부를 검색하는 데이터 처리도 자주 사용됩니다. 대표적으로 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공합니다.

  • findFirst()는 현재 스트림의 첫번째 요소를 Optional을 반환하며, findAny()는 순서와 상관없이 검색된 결과를 Optional을 반환함
  • anyMatch()는 적어도 한 요소와 일치하는지 확인하여 boolean을 반환함
  • allMatch()는 모든 요소가 일치하는지 확인하여 boolean을 반환하고, 이와 반대로 noneMatch()는 주어진 프레디케이트가 전혀 없는지 확인

private static Optional<Dish> findVegetarianDish() {
  return menu.stream().filter(Dish::isVegetarian).findAny();
}

리듀싱

대부분의 최종 연산은 Boolean, void, Optional 객체등을 반환했습니다. 또한 collect를 사용하면 스트림을 모아서 하나의 데이터 구조로 반환하는 방법도 있습니다. 그런데 스트림의 모든 요소에 반복적인 작업을 처리해야 할 때도 있는데, 이런 연산을 리듀싱(reduce) 연산(스트림 요소를 처리해서 값으로 반환하는 방법)이라 합니다.

  • reduce()BinaryOperator<T> accumulator을 인수로 전달받아서 스트림 요소에 해당 연산을 적용하고 Optional로 해당 값을 반환

아래 예에서 볼 수 있듯이 요소의 합에 사용하는 reduce는 두 개의 인수(초깃값과 람다 표현식)가 필요합니다. 만약 초깃값이 없을 경우 Optional 객체를 반환합니다.


List<Integer> numbers = Arrays.asList(3, 4, 5, 1, 2);

int result = 0;
for(int x : numbers) {
  result += x;
}
System.out.println(result);

// 리듀싱 연산, BinaryOperator<T> 타입의 매개변수임을 주의하자
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(sum);

두 컬렉터는 스트림의 요소를 비교하는데 사용할 Comparator를 인수로 받습니다. 또한 스트림에 있는 객체의 숫자 필드의 합계(summingInt)나 평균(averagingInt) 등을 반환하는 연산에도 리듀싱 기능이 자주 사용되며, 이러한 연산을 요약 연산이라고 부릅니다.


Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);

menu.stream().collect(summingInt(Dish::getCalories); // 합
menu.stream().collect(averagingInt(Dish::getCalories); // 평균

private static IntSummaryStatistics calculateMenuStatistics() {
  return menu.stream().collect(summarizingInt(Dish::getCalories)); // 스트림 요약
}

private static String getShortMenuCommaSeparated() {
  return menu.stream().map(Dish::getName).collect(joining(", ")); // 문자열 연결
}

범용 리듀싱은 초기값, 변환함수, 람다 세개의 인수를 필요로 하고, 초기값이 없이 람다식으로만 이뤄진 범용 리듀싱의 경우 변환값이 Optional로 반환됩니다.


// 범용 리듀싱 1
menu.stream().collect(reducing(0, Dish::getCalories, (Integer i, Integer j) -> i + j));

// 범용 리듀싱 2
menu.stream().collect(reducing((d1, d2) -> d1.getCalories() > d2.getCalories() ? d1 : d2)).get();

기본형 특화 스트림

스트림 API에서 숫자를 효율적으로 다룰 수 있도록 기본형 특화 스트림(primitive stream specialization)을 제공합니다. IntStream, DoubleStream, LongStream에서 min, max, sum과 같은 숫자 관련 리듀싱 연산을 제공합니다. 이러한 특화 스트림을 사용하기 위해선 일반 스트림을 mapToInt, mapToDouble, mapToLong등을 사용해서 특화 스트림으로 변환해야 합니다.


int calories = menu.stream()
  .mapToInt(Dish::getCalories)
  .sum();
System.out.println("Number of calories:" + calories);

프로그램에서 특정 범위의 숫자를 이용할 때, IntStreamLongStream에서 range()rangeClosed()라는 두 가지 정적 메서드를 제공합니다. range()는 시작값과 종료값이 결과에 포함되지 않는 반면, rangeClosed()는 결과에 포함된다는 차이점이 있습니다.


IntStream evenNumbers = IntStream.rangeClosed(1, 100)
  .filter(n -> n % 2 == 0);

System.out.println(evenNumbers.count());

기본값을 사용한 스트림

스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 구별하기 위해서 이전에 값이 존재하는지 여부를 가리킬 수 있는 컨테이너 클래스 Optional을 사용하면 됩니다. Integer, String 등의 레퍼런스 형식으로 파라미터화할 수 있는 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전을 제공합니다.


OptionalInt maxCalories = menu.stream()
        .mapToInt(Dish::getCalories)
        .max();

System.out.println(maxCalories.orElse(0));

객체 스트림으로 복원

정수가 아닌 다른 값을 반환하고자 한다면 boxed() 메서드를 이용하여 특화 스트림을 일반 스트림으로 변환할 수 있습니다.


IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // 스트림을 숫자 스트림으로 변환
Stream<Integer>  stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환

Collector 인터페이스

  • supplier는 새로운 결과 컨테이너를 생성
  • accumulator는 결과 컨테이너에 요소를 추가
  • finisher는 최종 변환값을 결과 컨테이너로 적용
  • combiner는 두 결과 컨테이너를 병합
  • characteristics 메서드는 Characteristics 형식의 불변 집합을 반환으로 병렬로 리듀스할 것인지 그리고 병렬로 리듀스한다면 어떤 최적화를 선택해야 할지 힌트를 제공

public class ToListCollector<T> implements Collector<T, List<T>, List<T>> {

    @Override
    public Supplier<List<T>> supplier() {
        return () -> new ArrayList<T>();
    }

    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item);
    }

    @Override
    public Function<List<T>, List<T>> finisher() {
        return i -> i;
    }

    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH, CONCURRENT));
    }
}

커스텀 컬렉터


public Map<Boolean, List<Integer>> partitionPrimesWithInlineCollector(int n) {
    return Stream.iterate(2, i -> i + 1).limit(n)
            .collect(
                    () -> new HashMap<Boolean, List<Integer>>() {{
                        put(true, new ArrayList<Integer>());
                        put(false, new ArrayList<Integer>());
                    }},
                    (acc, candidate) -> {
                        acc.get(isPrime(acc.get(true), candidate))
                                .add(candidate);
                    },
                    (map1, map2) -> {
                        map1.get(true).addAll(map2.get(true));
                        map1.get(false).addAll(map2.get(false));
                    });
}

public static class PrimeNumbersCollector
        implements Collector<Integer, Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> {

    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
        return () -> new HashMap<Boolean, List<Integer>>() {{
            put(true, new ArrayList<Integer>());
            put(false, new ArrayList<Integer>());
        }};
    }

    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
            acc.get(isPrime(acc.get(true),
                    candidate))
                    .add(candidate);
        };
    }

    @Override
    public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
        return (Map<Boolean, List<Integer>> map1, Map<Boolean, List<Integer>> map2) -> {
            map1.get(true).addAll(map2.get(true));
            map1.get(false).addAll(map2.get(false));
            return map1;
        };
    }

    @Override
    public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
        return i -> i;
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(IDENTITY_FINISH));
    }
}

참고

스트림 관련 참고 사항