Post

[자바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을 반환하므로 최종연산.
📕 Optional이란
값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.
아무 요소도 반환하지 않을 때(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는 생산된 값에 대해 연속적으로 계산하지 않는다.
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)
    );
This post is licensed under CC BY 4.0 by the author.