[자바8] 데이터소스에서 추출된 연속요소, 스트림과 스트림의 다양한 연산
스트림 (Detail.ver)
- 데이터소스에서 추출된
- 실제 입력이나 출력이 묘사된 데이터의 흐름을 말한다.
- (컬렉션 or 배열 or I/O자원 등의) 데이터소스로부터 데이터를 소비한다.
- 연속요소이다.
- 정렬된 컬렉션으로 스트림을 생성하면 정렬이 유지된다.
🎯 데이터 처리 연산을 지원하기 위해
- 데이터 처리 연산
- *선언형으로 컬렉션 데이터를 처리할 수 있다. 🏆 가독성
- 데이터를 처리하는 구현 코드 대신 *질의로 표현할 수 있다.
- 즉, 어떻게 동작을 구현할 지 지정할 필요가 없으며, *db와 비슷한 연산을 지원한다.
- 데이터를 처리하는 구현 코드 대신 *질의로 표현할 수 있다.
- 🚀 스트림의 중요한 특징 🚀
- (1) 파이프라이닝
- 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다. 🏆 유연성
⚠️ 연산 파이프라인은 db의 질의와 비슷하다 - (2) 내부 반복 지원 (반복을 추상화)
- 내부 반복을 이용하면 작업을 병렬로 처리하거나 더 최적화된 순서로 처리할 수 있다. 🏆 성능
📙 컬렉션은 현재 자료구조가 포함하는 모든 요소을 메모리에 저장하는 자료구조다.
컬렉션의 모든 요소는 컬렉션에 추가되기 전에 계산되어야 한다.
📗 스트림은 사용자가 요청할 때만 요소를 계산하는 고정된 자료구조다.
스트림에 요소를 추가하거나 제거할 수 없다.
스트림은 게으르게 만들어지는 (lazy) 컬렉션과 같다.
컬렉션은 DVD에 저장된 영화📀라고 하면, 스트림은 인터넷으로 스트리밍📶하는 영화다.
따라서 컬렉션은 데이터를 어떻게 저장하고 접근할 것인지에 중점을 두고,
스트림은 데이터에 어떤 연산을 할 것인지 묘사(질의)하는 것에 중점을 둔다.
또한, 탐색된 스트림 요소는 소비된다.
즉, 한번 탐색한 요소를 다시 탐색하려면 초기 데이터소스에서 새로운 스트림을 만들어야 한다.
⚠️ 단, 데이터소스가 I/O채널이라면 소스를 반복사용할 수 없음으로, 새로운 스트림을 만들 수 없다.
스트림 연산
중간연산: 서로 연결되어 파이프라인을 형성한다.
스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다.
중간 연산을 합치고, 합친 중간 연산은 최종 연산으로 한번에 처리하여,
최적의 효과를 얻을 수 있다, ex. 쇼트 서킷
map, filter, sorted, distinct, limit ...
최종연산: 파이프라인을 실행한 다음 닫는다.
collect, reduce, foreach, count, anyMatch, findAny ...
필터링
filter
: 람다를 인수로 받아, 모든 스트림에 Predicate를 적용해 특정 요소를 제외distinct
: 고유 여부 필터링, 고유 여부는 스트림에서 만든 객체의hashCode
,equals
로 결정된다.
슬라이싱
takeWhile
: 정렬되어 있는 경우, 람다를 인수로 받아, 모든 스트림에 Predicate를 적용해 슬라이스dropWhile
: 람다를 인수로 받아, takeWhile과 정반대 작업 수행limit
: 정해진 개수 이하의 크기를 갖는 새로운 스트림 반환(축소)skip
: 처음 n개의 요소를 제외한 스트림 반환
매핑(특정 데이터 선택/추출)
map
- 람다를 인수로 받아, 각 요소를 다른 요소로 변환하거나 정보를 추출
- 배열 스트림을 반환
flatMap
- 람다를 인수로 받아, 중복된 스트림을 하나의 스트림으로 평면화
- 각 배열 스트림이 아닌 스트림의 콘텐츠로 매핑
⚠️ Arrays.stream: 문자열을 받아 스트림으로 만든다.
📜 FindSpellingInWords.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<String> words = Arrays.asList("Hello", "World");
words.stream()
.map(word -> word.split("")) // 각 단어를 개별 문자열 배열로 반환, Stream<String[]>
.map(Arrays::stream) // 각 문자열 배열을 별도의 스트림으로 생성, Stream<Stream<String>>
.distinct() // Stream<Stream<String>>
.collect(toList());
// 결과: List<Stream<String>>
words.stream()
.map(word -> word.split("")) // 각 단어를 개별 문자열 배열로 반환, Stream<String[]>
.flatMap(Arrays::stream) //생성된 별도의 스트림을 하나의 스트림으로 평면화, Stream<Stream<String>> -> Stream<String>
.distinct() // Stream<String>
.collect(toList());
// 결과: List<String>
📜 OrderedPair.java
1
2
3
4
5
6
7
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs = numbers1.stream()
.flatMap(i -> numbers2.stream()
.map(j -> new int[] {i, j}))
.collect(toList());
검색과 매칭
anyMatch
- 람다를 인수로 받아, Predicate를 적용해 적어도 하나의 요소와 일치하는지 확인
- 불리언을 반환하므로 최종연산.
allMatch
- 람다를 인수로 받아, Predicate를 적용해 모든 요소가 일치하는지 확인
- 불리언을 반환하므로 최종연산.
nonMatch
- 람다를 인수로 받아, allMatch와 반대 연산을 수행
- 불리언을 반환하므로 최종연산.
findFirst
- 첫번째 요소 찾기, Optional
반환 - Optional을 반환하므로 최종연산.
findAny
- 현재 스트림에서 임의의 요소 반환, Optional
반환 - findFirst가 있는데 굳이 왜 있을까
- 🏆 병렬성 : 병렬실행에서는 첫번째 요소를 찾기 애매하다.
- 따라서, 요소의 반환 순서가 상관없다면, 병렬스트림에서 제약이 적은 findAny를 사용
- Optional을 반환하므로 최종연산.
- findFirst가 있는데 굳이 왜 있을까
값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.
아무 요소도 반환하지 않을 때(null) 에러를 일으키지 않게, null 확인 버그를 피하는 방법을 설계했다.
즉, 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공
isPresent(): 값이 존재하면 참을 반환, 반대는 거짓 반환
ifPresent(Consumer<T> block): 값이 있다면 주어진 블록을 실행
get(): 값이 존재하면 값을 반환, 없다면 NoSuchElementException
orElse(T other): 값이 존재하면 값을 반환, 없다면 기본값 반환
때로는, 전체 스트림을 처리하지 않고도 결과를 즉시 반환할 수 있다.
takeWhile, anyMatch, allMatch, nonMatch, findFirst, findAny, limit 같은 연산은
모든 요소를 처리할 필요없을 때가 있다.
* 리듀싱 (스트림의 모든 요소를 반복 조합하여 값을 도출하는 질의)
reduce(초기값, 람다)
- 스트림의 모든 요소를 소비할 때까지 각 요소를 반복해서 조합
- ⚠️누적값으로 람다를 다시 호출하며
reduce(람다)
- 초기값을 받지 않도록 오버로드된 reduce도 있는데,
- 스트림에 아무 요소도 없는 상황에서는 초기값이 없음으로 합계를 반환할 수 없다.
- 따라서 합계가 없음을 가리킬 수 있도록
Optional객체
로 감싼 결과를 반환 - 스트림에 아무 요소도 없는 상황에서는 초기값이 없음으로 합계를 반환할 수 없다.
📜 MapReducePattern.java
- 🎯 쉽게 병렬화할 수 있다.
1
2
3
4
5
6
int count = menu.stream()
.map(a - > 1)
.reduce(0, (a, b) -> a + b);
// 사실은 count()가 있긴 하다
long count = menu.stream().count();
Stateless / Stateful Operation
map
filter
같은 연산은 입력스트림에서 각 요소를 받아 결과를 출력스트림으로 보낸다.
따라서, 보통 *내부상태를 갖지 않는 연산이다. stateless operation
reduce
sum
max
와 같은 연산은 결과를 *누적할 내부 상태가 필요하다.
단, 스트림에서 처리하는 요소의 개수와 상관없이, *내부 상태 크기는 한정되어 있다. bounded
sorted
distinct
연산은 map, filter 와는 다르다.
정렬하거나 중복을 제거하려면 *과거의 이력을 알아야 하고,
따라서 어떤 요소를 출력스트림으로 추가하려면, *모든 요소가 버퍼에 추가되어 있어야 한다. stateful operation
단, 연산을 수행하는데 필요한 저장소 크기는 정해져 있지 않음으로
스트림의 크기가 너무 크거나 무한이라면 문제가 생길 수 있다.
스트림 만들기
(1) 컬렉션에서 스트림 얻기 컬렉션.stream()
(2) 범위 숫자 스트림 만들기 range(시작값, 종료값)
rangeClosed(시작값, 종료값)
1
2
3
IntStream evenNumbers = IntStream.rangeClosed(1, 100) // 1이상 100이하
.filter(n -> n % 2 == 0);
.count();
(3) 값으로 스트림 만들기 Stream.of()
Stream.empty()
1
2
Stream<String> stream = Stream.of("Hello", "World");
stream.empty(); // 스트림을 비울 수도 있다
(4) 배열로 스트림 만들기 Arrays.stream()
1
2
3
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
// int로 이루어진 배열은 IntStream으로 변환된다.
- (5) 파일로 스트림 만들기
Files.lines(Path객체, Charset객체)
- 파일을 행단위로 읽어들이고, 스트림 반환
- ⚠️ Stream 인터페이스는 AutoClosable 인터페이스를 구현한다.*
1
2
3
4
5
6
7
8
9
long uniqueWords = 0; // 고유한 단어수 찾기
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) {
//...
}
- (6) 크기가 고정되지 않은 무한 스트림 만들기
Stream.iterate
Stream.generate
- 요청할 때마다 값을 계산함으로써, 무제한으로 생성할 수 있다.
- 보통 무한한 결과를 계산하지 않도록 limit 함수를 함께 연결해서 사용.
iterate
는 생산된 값에 대한 연속적인 계산이 가능하다. (UnaryOperator<T>를 인수로 받는다)- 연속적인 계산을 통해 무한스트림을 만드는 이러한 스트림을 언바운드 스트림이라 한다.
generate
는 생산된 값에 대해 연속적으로 계산하지 않는다. - 보통 무한한 결과를 계산하지 않도록 limit 함수를 함께 연결해서 사용.
1
2
3
4
5
6
7
8
9
10
// Stream.iterate 예제, 피보나치 수열
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);
// Stream.generate 예제, 난수
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
효율성을 위한, 기본형 특화 스트림
내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.
이때 박싱비용이 든다.
1
2
3
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
int요소에 특화된 IntStream
변환: mapToInt
double요소에 특화된 DoubleStream
변환: mapToDouble
long요소에 특화된 LongStream
변환: mapToLong
🔮 기본형 특화 스트림은 숫자 관련 리듀싱 연산 메서드도 제공한다.
sum max min
⚠️ 스트림이 비어있으면 기본값(0) 반환
1
2
3
int calories = menu.stream() // Stream<Dish> 반환
.mapToInt(Dish::getCalories) // IntStream 변환
.sum();
💣 리듀싱 연산 메서드 결과가 0인 경우와 어떻게 비교?
OptionalInt OptionalDouble OptionalLong
1
2
3
4
5
6
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(-1);
// 최댓값이 없는 상황에서 사용할 기본값 명시적으로 정의
🔮 기본형 특화 스트림을 다시 객체 스트림으로 복원할 수도 있다.
boxed mapToObj
1
2
3
4
5
6
7
8
9
10
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // 스트림을 기본형 특화 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 기본형 특화 스트림을 스트림으로 변환
String<int[]> pythagoreanTriples = IntStream.rangeClosed(1, 100).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 100)
.mapToObj(
b -> new double[] {a, b, Math.sqrt(a*a + b*b)}
)
.filter(t -> t[2] % 1 == 0)
);