[모던 자바 인 액션] 스트림 활용

이주오·2021년 8월 16일
0

도서

목록 보기
5/15

스트림 API가 지원하는 다양한 연산들을 살펴보자.

스트림의 요소 걸러내기


filter()

  • Predicate로 필터링
  • filter 메서드는 Predicate를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
    Stream<T> filter(Predicate<? super T> predicate);
  • 다른 조건으로 여러 번 사용 가능하다.
    intStream.filter(i -> i%2 != 0 && i%3 != 0)...
    intStream.filter(i -> i%2 != 0).filter(i%3 != 0)...

distinct()

  • 고유 요소로 이루어진 스트림을 반환한다.
    • 즉 중복된 요소들을 제거
    • 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.
    Stream<T> distinct();

정렬


sorted()

  • 스트림을 정렬할 때 사용하는 메서드
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator);
  • Comparator 대신 int 값을 반환하는 람다식을 사용하는 것도 가능하다.
  • Comparator를 지정하지 않으면 스트림 요소의 Comparable으로 정렬한다.
    • 만약 Comparable을 구현한 클래스가 아니라면예외가 발생한다
  • jdk 1.8부터 Comparator 인터페이스에 정렬에 도움을 주는 static 메서드와 디폴트 메서드가 많이 추가되었다.
    • default
      • reversed
      • thenComparing
      • theComparingInt..
    • static
      • comparing
      • comparingInt..
  • 예를 들어 선수 스트림을 팀별, 성적순, 이름순으로 정렬하려면??
playerStream.sorted(Comparator.comparing(Player::getTeam)
		.thenComparing(Player::getScore)
		.thenComparing(Player::getName))
        .forEach(System.out::println);

스트림 슬라이싱


자바9에서 추가된 메서드인 takeWhile(), dropWhile()

takeWhile()

  • Predicate를 통한 슬라이싱이다.
  • 무한 스트림을 포함한 모든 스트림에 Predicate를 적용해 Predicate의 결과가 true인 동안 요소를 가져와 스트림을 슬라이스 할 수 있다.
  • 즉 Predicate와 일치하지 않는 요소를 발견하면 스트림의 나머지 부분이 버려진다.
    default Stream<T> takeWhile(Predicate<? super T> predicate)
  • 칼로리가 300보다 작은 요소들 구성된 스트림으로 슬라이스
    List<Food> slicedMenu = calorieSortedMenu.stream()
    					     .takeWhile(dish -> dish.getCalories() < 300)
    														.collect(toList());

dropWhile()

  • Predicate를 통한 슬라이싱이다.
  • takeWhile()과 정반대의 작업을 수행한다.
  • Predicate이 처음으로 거짓이 되는 지점까지 발견된 요소를 버리고 작업을 중단하고 남은 모든 요소를 반환한다.
  • 마찬가지로 무한스트림에서도 동작
    default Stream<T> dropWhile(Predicate<? super T> predicate) 
  • 300 칼로리보다 높은 요소들로 구성된 스트림으로 슬라이스
    List<Food> slicedMenu = calorieSortedMenu.stream()
					     .dropWhile(dish -> dish.getCalories() < 300)
					     .collect(toList());

limit()

  • 스트림 축소
  • 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다.
  • 정렬되지 않은 스트림에도 사용가능하다.
    Stream<T> limit(long maxSize);

skip()

  • 요소 건너뛰기
  • 처음 n개 요소를 제외한 스트림을 반환한다.
  • 만약 n개 이하의 요소를 포함하는 스트림에 호출하면 빈 스트림이 반환된다.
    Stream<T> skip(long n);

매핑


특정 객체에서 특정 데이터를 선택하는 처리 과정에서 자주 수행되는 연산

Map()

  • 스트림의 각 요소에 함수 적용하기 위한 메서드
  • Function을 인수로 받아 각 요소에 적용한 결과가 새로운 요소로 매핑된 스트림을 반환한다.
  • 기본형 요소에 대한 mapToType 메서드도 지원한다 (mapToInt, mapToLong, mapToDouble).
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
  • 각 단어들의 글자 수의 리스트를 반환
    List<Integer> wordLengths = Arrays.asList("one", "two", "three").stream()
								    .map(String::length)
								    .collect(toList());

flatMap()

  • 스트림의 요소가 배열기나 map()의 연산결과가 배열인 경우, 즉 스트림의 타입이 Stream<T[]>인 경우 Stream<T>로 다루고 싶거나 Stream가 더 편리할 때 사용하는 메서드

  • 스트림의 각 값을 다른 스트결과적으로 하나의 평면화된 스트림을 반환한다.


한번 살펴보자!!

  • 해당 String 중에 길이가 5 이상인 String을 출력해보자
String[][] wordArr = new String[][]{
	 new String[]{"team", "victory", "fighting"},
	 new String[]{"flatMap", "is", "too", "difficult"}
};
  • map 사용
    Arrays.stream(wordArr) // Stream<String[]>
                    .map(innerArray -> Arrays.stream(innerArray)) // Stream<Stream<String>>
                    .forEach(innerStream -> innerStream.filter(word -> word.length() >  5) // Stream<String>
                            .forEach(System.out::println));

  • flatMap 사용
    Arrays.stream(wordArr) //  Stream<String[]>
                    .flatMap(innerArray -> Arrays.stream(innerArray)) // // Stream<String>
                    .filter(word -> word.length() > 5)
                    .forEach(System.out::println);


stream으로 카드쌍 만들기

List<String> kinds = Arrays.asList("스페이드", "하트", "다이아몬드", "클로버");
List<String> nums = Arrays.asList("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K");
List<String[]> cards = kinds.stream()
			    .flatMap(kind -> nums.stream()
						 .map(num -> new String[] {kind, num}))
			    .peek(arr -> System.out.println(Arrays.toString(arr)))
                            .collect(Collectors.toList());
  • peek는 forEach와 달리 스트림 요소를 소모하지 않는다.


검색과 매칭 (쇼트 서킷)


특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리 유틸리티 메서드를 제공한다.

anyMatch

  • 적어도 한 요소와 일치하는지 확인하는 메서드
  • boolean 반환값이므로 최종 연산이다.
boolean anyMatch(Predicate<? super T> predicate);

allMatch

  • 모든 요소와 일치하는지 검사하는 메서드
  • 마찬가지로 최종 연산이다
boolean allMatch(Predicate<? super T> predicate);

noneMatch

  • allMatch와 반대 연산
  • 즉, 모든 요소가 Predicate 와 일치하지 않으면 true
boolean noneMatch(Predicate<? super T> predicate);

anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.

findAny

  • 현재 스트림에서 임의의 요소를 반환한다.
  • 마찬가지로 최종연산이며 쇼트서킷을 이용해서 결과를 찾는 즉시 실행 종료
Optional<T> findAny();

findFirst

  • 첫 번째 요소를 찾아 반환한다. 순서가 정해져 있을 때 사용한다.
Optional<T> findFirst();

그렇다면 findAny와 findFirst는 언제 사용하는 메서드일까??

  • 바로 병렬성 때문이다.
  • 병렬 실행에서는 첫 번째 요소를 찾기 어렵기 때문에 findFirst 메서드를 사용하고
  • 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

참고 - Optional이란??

  • Optional는 값의 존재나 부재 여부를 표헌하느 컨테이너 클래스이다.
  • 앞서 본 findAny 혹은 findFirst 메서드는 아무 요소도 반환하지 않을 수 있으므로 NPE가 발생할 수 있다.
  • 주요 메서드
    • isPresent() : Optional이 값을 포함하면 true, 아니라면 false
    • ifPresent(Consumer<T> block) : 값이 있으면 주어진 블록 실행
    • T get() : 값이 존재하면 값 반환, 없으면 NoSuchElementException
    • T orElse(T other) : 값이 있으면 값 반환, 없으면 기본값 반환

리듀싱


reduce

  • 모든 스트림의 요소를 줄여나가면서 연산을 수행하고 최종결과를 반환하는 메서드
  • reduce()는 두개의 인수를 갖는다.
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
  • 초기값 T
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator
  • combiner : 병렬처리된 결과를 합치는데 사용할 연산(병렬 스트림)
public interface BiFunction<T, U, R> {
	R apply(T t, U u);
}

 @FunctionalInterface
 public interface BinaryOperator<T> extends BiFunction<T,T,T> { ... }
  • 초기값스트림의 첫 요소를 가지고 연산한 결과(누적 값, accumulated value)를 가지고 그 다음 요소와 연산한다.
  • 이 과정에서 스트림의 요소를 하나씩 소모하게 되며, 모든 요소를 소모하게 되면 그 결과를 반환한다.
  • 최정연산 count(), sum() 등은 내부적으로 모두 reduce()를 이용해서 작성된 것이다.

reduce를 이용한 요소의 합

int sum = 0;
for(int x : numbers) {
		sum += x;
}
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum = numbers.stream().reduce(0, Integer::sum);


이미지 출처

  • 0 + 4 의 결과인 4가 새로운 누적값

  • 누적값으로 람다를 다시 호출하며 다음 요소인 5를 소비

  • 반복하며 마지막 요소 9로 람다를 호출하면 최종적으로 21


초기값이 없는 경우

Optional<T> reduce(BinaryOperator<T> accumulator);
  • 처음 두 요소를 가지고 연산한 누적값을 사용

  • Optional 객체 반환한다.

    • 왜일까??
    • 스트림에 아무 요소도 없는 경우 초기값조차 없다면 합계가 없음을 나타낼 수 있도록 Optional 객체로 감싼 결과를 반환

최대값과 최소값

Option<Integer> max = numbers.stream().reduce(Integer::max);
Option<Integer> min = numbers.stream().reduce(Integer::min);

map - reduce

int count = menu.stream().map(d -> 1).reduce(0, (a, b) -> a + b);

map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라 하며, 쉽게 병렬화하는 특징을 이용한 것을 구글이 발표하면서 유명해졌다.

기존 코드에 비해 reduce 메서드의 장점은??
바로 reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다. 기존 코드에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵고 동기화의 cost가 매우 크기 때문이다.

스트림 연산 : 상태 없음 vs 상태 있음

  • 스트림을 이용해서 연산을 쉽게 구현할 수 있으며 parrell 메서드를 통해 쉽게 병렬성을 얻을 수 있다.

  • 하지만 스트림 연산은 각각 다양한 연산을 수행하기 때문에 내부적인 상태를 고려해야 한다.

  • 내부 상태가 없는 연산

    • 사용자가 제공한 람다나 메서드 참조가 내부적인 가변 상태를 갖지 않는다는 가정하에
    • map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
    • 따라서 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산이다.
  • 내부 상태가 있는 연산

    • reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다.
    • 스트림에서 처리하는 요소 수와 관계없이 내부 상태의 크기는 한정되어 있다.
    • sorted, distinct는 값을 비교하기 위해 모든 요소가 버퍼에 추가되어 있어야 함.

숫자형 스트림


  • 스트림 API는 오토박싱 & 언박싱으로 인한 비용을 줄이기 위해 기본형 특화 스트림(primitive stream specialization)을 제공한다.

  • sum, max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.


기본형 특화 스트림

  • 기본형 특화 스트림으로 IntStreamDoubleStreamLongStream이 존재한다.
  • 각각의 인터페이스에는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 메서드를 제공한다.
int sum()
OptionalDouble average()
Optional*Int* max()
Optional*Int* min()
  • 해당 메서드들은 최종연산인 것을 잊지 말아야 한다

  • sum 메서드를 제외한 나머지 메서드들은 요소가 없을 때 0을 반환할 수 없으므로 이를 구분하기 위해 Optional 래퍼 클래스를 반환

  • Optional도 기본형에 대하여 지원한다. OptionalIntOptionalDoubleoptionalLong 세 가지 기본형 특화 스트림 버전의 Optional이 제공된다.


숫자 스트림으로 매핑

IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction<? super T> mapper);

객체 스트림으로 복원하기

  • boxed메서드를 이용하면 특화 스트림을 일반 스트림으로 변환할 수 있다.
Stream<Integer> boxed(); // IntStream to Stream<Integer>
<U> Stream<U> mapToObj(IntFunction<? extends U> mapper); // IntStream to Stream<U>

스트림 만들기


이제 스트림으로 작업하기 위해 스트림을 생성하는 다양한 방법들을 알아보자

컬렉션

  • 컬렉션의 최고 조상인 Collection 인터페이스의 stream()
default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

값으로 스트림 만들기

  • 정적 메서드 Stream.of 을 이용하여 스트림을 만들 수 있다.
public static<T> Stream<T> of(T... values) {
		return Arrays.stream(values);
}
Stream<String> strStream = Stream.of("hello", "world";)

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

  • 자바 9부터 지원되며 Stream.ofNullable 메서드를 이용하여 null이 될 수 있는 객체를 지원하는 스트림을 만들 수 있다.
  • null이면 빈 스트림 반환
public static<T> Stream<T> ofNullable(T t) {
		 return t == null ? Stream.empty()
											: StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false);
}

배열로 스트림 만들기

  • 배열을 인수로 받는 정적 메서드 Arrays.stream 을 이용하여 스트림을 만들 수 있다.
public static <T> Stream<T> stream(T[] array)
public static <T> Stream<T> stream(T[] array, int startInclusive, int endExclusive)
public static *Int*Stream stream(int[] array)
public static *Int*Stream stream(int[] array, int startInclusive, int endExclusive)

특정 범위의 정수 스트림 만들기

  • 특정 범위의 숫자를 이용해야 할 때 range와 rangeClosed 메서드를 사용할 수 있다.
  • IntStream, LongStream 두 기본형 특화 스트림에서 지원된다.
  • range는 end가 범위에 포함되지 않으며, rangeClosed는 포함한다.
public static IntStream range(int startInclusive, int endExclusive)
public static IntStream rangeClosed(int startInclusive, int endInclusive)

파일로 스트림 만들기

  • 자바의 NIO API(논블록 I/O)도 스트림 api를 활용할 수 있다.
  • java.nio.file.Files의 많은 static 메서드가 스트림을 반환한다.
Stream<Path> Files.list(Path dir)
Stream<String> Files.lines(Path path)

함수로 무한(언바운드) 스트림 만들기

  • Stream.iterateStream.generate를 통해 함수를 이용하여 무한 스트림을 만들 수 있다.
  • iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다.
  • 따라서 무제한으로 값을 계산할 수 있지만, 보통 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결해서 사용한다.
  • Stream.iterate
    • 초기값과 람다식 f에 의해 계산된 결과를 다시 seed 값으로 해서 계산을 반복한다.
    public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
    public static<T> Stream<T> iterate(T seed, Predicate<? super T> hasNext, UnaryOperator<T> next)
    Stream<Integer> evenStream = Stream.iterate(0, n -> n + 2) // 0, 2, 4, 6, ...
    // 0 -> 0 + 2
    // 2 -> 2 + 2
    // 4 -> 4 + 2
    // ....
  • Stream.generate
    • Supplier를 인수로 받아서 새로운 값을 생산한다.
    public static<T> Stream<T> generate(Supplier<? extends T> s)
    Stream<Double> randomStream = Stream.generate(Math::random);
  • Random 클래스의 메서드들 이용
    public IntStream ints(long streamSize)
    public LongStream longs(long streamSize)
    public DoubleStream doubles(long streamSize)

    InStream intStream = new Random().ints(); // 무한 스트림 o
    InStream intStream = new Random().ints(5); // 무한 스트림 x

무한 스트림을 이용해서 피보나치 수열을 만들어보자

  • iterate()
    IntStream.iterate(new int[] {0, 1}, t -> new int[] {t[1], t[0] + t[1]})
          .limit(20)
    			.map(t -> t[0])
          .forEach(System.out::println);
  • generate()
    IntSupplier fib = new IntSupplier() {
    		private int prev = 0;
    		private int cur = 1;
    		@Override
    		public int getAsInt() {
    		    int oldPrev = this.prev;
    		    int nextValue = this.prev + this.cur;
    		    this.prev = this.cur;
    		    this.cur = nextValue;
    		    return oldPrev;
    		}
    };

    IntStream.generate(fib)
              .limit(20)
  • IntSupplier 인스턴스는 기존 피보나치 요소와 두 인스턴스 변수에 어떤 요소가 들어있는지 추적하므로 가변 상태 객체다.
  • iterate를 사용했을 때는 각 과정에서 새로운 값을 생성하면서 기존 상태를 바꾸지 않는 순수한 불변 상태를 유지했다.
  • 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야 한다.
profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글