스트림 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..
- default
- 예를 들어 선수 스트림을 팀별, 성적순, 이름순으로 정렬하려면??
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, 아니라면 falseifPresent(Consumer<T> block)
: 값이 있으면 주어진 블록 실행T get()
: 값이 존재하면 값 반환, 없으면 NoSuchElementExceptionT 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 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.
기본형 특화 스트림
- 기본형 특화 스트림으로
IntStream
,DoubleStream
,LongStream
이 존재한다. - 각각의 인터페이스에는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 메서드를 제공한다.
int sum()
OptionalDouble average()
Optional*Int* max()
Optional*Int* min()
해당 메서드들은 최종연산인 것을 잊지 말아야 한다
sum 메서드를 제외한 나머지 메서드들은 요소가 없을 때 0을 반환할 수 없으므로 이를 구분하기 위해 Optional 래퍼 클래스를 반환
Optional도 기본형에 대하여 지원한다.
OptionalInt
,OptionalDouble
,optionalLong
세 가지 기본형 특화 스트림 버전의 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.iterate
와Stream.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)
- Supplier
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를 사용했을 때는 각 과정에서 새로운 값을 생성하면서 기존 상태를 바꾸지 않는 순수한 불변 상태를 유지했다.
- 스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야 한다.
'java' 카테고리의 다른 글
[모던 자바 인 액션] 병렬 데이터 처리와 성능 (0) | 2024.07.17 |
---|---|
[모던 자바 인 액션] 스트림으로 데이터 수집 (0) | 2024.07.17 |
[모던 자바 인 액션] 스트림 소개 (0) | 2024.07.07 |
[모던 자바 인 액션] 람다 표현식 (0) | 2024.07.07 |
[모던 자바 인 액션] 동작 파라미터화 (0) | 2024.07.07 |