반응형

스트림(Stream)

  • 다양한 데이터 소스(컬렉션, 배열)를 표준화된 방법으로 다루기 위한 것

CollectionFramwork 는 List, Set, Map 등 다양한 컬렉션 클래스들을 표준화된 방법으로 다루려고 정리한 것이다. 그런데, 사실 실패했다. 왜냐하면, List,Set 과 Map이 성격이 달라서 사용방법이 달랐기 때문이다. 그러니깐, 말만 표준화지, 반쪽짜리 표준화였던 것이다.

그런데, 드디어 JDK1.8부터는 스트림 이라는 것이 나와서 정말로 진정한 통일이 되었다.
다양한 데이터소스를 다루는 방법이 정말로 통일 되었다.

그래서 컬렉션에는 List, Set, Map 이 있고, 배열이 있는데, 이러한 다양한 데이터 소스로부터 Stream을 만들 수 있다.

일단 Stream을 만들고나면, 똑같은 방식으로 작업을 처리하게 된다.

중간연산과 최종 연산이 있는데, 중간 연산은 n번, 최종 연산은 딱 1번만 할 수 있다.

스트림으로 변환하는 방법이다. 즉, 스트림을 생성하는 방법이다.

Collection에는 .stream() 이라는 메서드가 있다. 컬렉션을 스트림으로 반환해서 변환하는 것이다.
그래서 이런식으로 모든 종류의 컬렉션을 쉽게 스트림으로 바꿀 수 있다.

Stream.of 를 이용해서 배열도 스트림으로 바꿀 수 있다. 

람다식을 이용해서도 스트림을 만들 수 있다.

난수들로 이루어져있는 스트림을 만드는 방법도 있다.

 

그래서, 우리가 스트림으로 작업할 때,

1. 스트림 만들기

2. 중간 연산(n번)

3. 최종 연산 1번만

그 후 결과를 얻어낸 식으로 코드를 작성하게 된다.

Stream의 뜻이 뭐냐면, 
청계천 같은 시냇물을 Stream이라고 하는데,

마찬가지로 데이터소스가 있을 때, Stream을 생성하면, 데이터 소스가 가지고 있는 데이터들이 스트림을 통해서 연속적으로 하나씩 하나씩  전달되서 우리가 중간작업 -> 최종작업 이렇게 처리하는데, 

데이터 소스로부터 데이터들이 스트림을 통해서 하나씩 전달되는 이러한 특성떄문에 Stream이라는 이름이 붙었다.

Stream은 데이터의 연속적인 흐름이다.

 

  • 스트림이 제공하는 기능 - 중간 연산과 최종 연산

1. 스트림 만들기

2. 중간 연산(0~n번)

3. 최종 연산(0~1번)

앞서 스트림을 만들었고, 
우리가 할 일은 중간 연산과 최종 연산을 하는 것이다.

중간 연산은 연산 결과가 스트림 그자체다. 그래서 여러번 적용할 수 있다. (0~n번)

최종 연산은 스트림에서 데이터를 하나씩 꺼낸다.(스트림의 요소를 소모한다.) 그래서 딱 1번만 적용가능하다.(0~1번)

stream으로부터 중간연산을 3번 했고, 최종연산은 1번 했다.
중간 연산에서 distinct()는 중복제거, limit(5)는 5개 자르기, sorted()는 정렬이다. 이러한 작업들이 중간연산이고,
최종 연산은, .forEach(System.out::pringln) 이다. 출력이 최종연산은 아닌데, forEach로 하나씩 꺼내서  출력하는 것이다.

중간연산은 실행결과가 스트림이라서 계속 이어서 쓸 수 있다.

 


예를 들어서 문자배열이 있는데, 
1. 문자열 배열로부터 스트림을 만들고, 

2. 중간연산을 하고,
filter()는 걸러내기,
distinct()는 중복제거,
sort()는 정렬,
limit(5)는 스트림 자르기(5개로)

3. 최종연산을 한다. 

 


 

스트림(Stream)의 특징

  • 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않는다.

스트림은 데이터 소스의 원본을 변경하지 않는다. 읽기만한다.

 

3, 1, 5, 4, 2라는 데이터를 가진 리스트가 있는데, 
리스트를 stream으로 바꿔서 정렬하고, 다시 새로운 List에 저장했다.

이 작업을 수행해도, 기존에 list는 변하지 않는다.

 

  • 스트림은 Iterator 처럼 일회용이다. (필요하면 다시 스트림을 생성해야 함)

스트림은 Iterator 처럼 일회용이라서, 필요하면 다시 스트림을 생성해서 사용해야 한다.
Iterator는 컬렉션의 모든 요소를 읽을 때 사용하는데, 다 읽고나면 Iterator를 다시 만들어서 사용해야 한다.

마찬가지로 스트림도 일회용이다.

stream에 대하여 최종연산을 하고나면, 스트림의 요소를 소모한다. 하나씩 꺼내서 사용하기 때문이다.
그래서, 최종연산을 하고나면, 더이상 사용할 요소가 없다.
즉, 최종연산을 하고나면, 스트림이 닫힌다.

만약 그다음에, 해당 스트림을 가지고 또 최종연산을 하려고하면, 에러가발생한다. (스트림이 이미 closed)

 

  • 최종 연산 전까지 중간연산이 수행되지 않는다. - 지연된 연산

스트림에는 유한 스트림과 무한스트림이 있는데, 여기서 1~45범위의 난수를 발생시키는 스트림이 있는데, 이것은 무한 스트림이다.(유한스트림과 무한스트림의 차이는 뒤에서 알아볼 것이다.)

이 스트림은 우리가 난수를 필요한 만큼 달라는 만큼 끝도없이 준다. 그래서 무한스트림인데,
무한스트림에서 중간연산을 했는데, distinct, limit, sorted를 수행했다.

그리고나서 최종연산으로 출력을 했다.

위의 코드는 로또 번호를 출력하는 코드인데,
무한 스트림에서 중복제거를 할 수 있을까? 안된다. 무한스트림은 무한히 스트림의 요소가 있는 것이기 때문이다.

그런데도, 위의 코드처럼 distinct 가 가능하다. 왜 가능하냐면,  
중간연산으로 중복제거하고, 자르고, 정렬하라고 했지만,  
이 스트림이 지금 당장 중복을 제거하고, 6개 자르고, 정렬을 수행하는 것이 아니다.

중간연산 메서드가 호출되었을 때 스트림을 바로 처리하는 것이 아니라,
이 스트림을 가지고 어떤 작업을 해야하는지 표시만 해놓은 것이다.
그리고 나중에 필요할 때 수행하게 된다.

이것을 바로 "지연된 연산" 이라고 한다.

그래서 코드만 봤을 때는 "무한스트림에서 어떻게 중복을 제거해?" 라고 말이 안된다고 생각할 수 있지만,
이러한 코드가 가능한 이유는, "지연된 연산"을 하기 때문이다.

 

  • 스트림은 작업을 내부 반복으로 처리한다.

스트림은 반복을 내부반복으로 처리한다.

이렇게 반복문으로 처리할 것을 forEach라는 최종연산을 이용하여 간단히 처리할 수 있다.
for문을 메서드안에 감춰버린 것이다. 성능은 비효율적이지만, 코드가 간결해진다.

 

  • 스트림의 작업을 병렬로 처리 - 병렬 스트림

병렬 스트림이란, 멀티쓰레드로 병렬처리 하는 것이다.

함수형 언어(FP)의 특징은 빅데이터 처리에 좋은데, 큰 작업을 빠르게 처리하려면, 한쓰레드가 처리하는 것 보다, 여러개의 쓰레드가 나눠서처리하는 것이 더 빠르게 처리가 될 것이다. 이러한 기능을 제공하기 위해 병렬 스트림이라는 기능을 제공한다.

스트림에다가 parallel() 이라고 메서드 호출하고,  그 다음 작업들은 똑같이 처리하면, 병렬로 처리되어 빠르게 결과를 얻을 수 있다.
스트림을 병렬 스트림으로 변환하여 사용하는 것이다.
이 반대로 변환 하는 것은 Sequential() 인데, 기본이 Sequential() 이다.

 

  • 기본형 스트림 - IntStream, LongStream, DoubleStream ...
    • 오토박싱 & 언박싱의 비효율이 제거됨 (Stream<Integer> 대신 IntStream 사용)
    • 숫자와 관련된 유용한 메서드를 Stream<T> 보다 더 많이 제공

Stream<Integer>는 스트림의 요소가 Integer 타입이라는 것이다. 
그런데, 원래 스트림 선언부는 Stream<T> 인데, T기본형은 올 수 없다. 참조형만 가능하다.
그래서 우리가, int배열 {1, 2, 3}이 있을때, 타입이 int인데, 이것을 스트림으로 바꾸면,  
1 -> new Integer(1) 즉 기본형 -> 참조형 으로 바뀌는 것이다.

이처럼 어떤 스트림으로 변환하면(스트림을 만들면) 기본형이 참조형으로 바뀌어서 저장되는 것이다.

기본형으로 참조형으로 변환되는 것을 9장에서 "오토박싱"이라고 배웠다.
그리고 계산할 때는 또 언박싱을 해서 계산할 것이다.

그래서 이러한 변환이 필요하고, 여기서 시간이 소요가 되는데, 
아까 말했듯, 빅데이터 처럼 데이터가 많으면 이 시간을 무시하지 못한다.

그래서 효율을 높이기 위해 숫자일 때는 Stream<Integer> 대신 IntStream을 사용한다.

LongStream도 있고, DoubleStream도 있다. 기본형은 다 있다.

이러한 것들을 제공하는 이유가 오토박싱(기본형에서 객체로)과 언박싱(객체에서 기본형으로) 바꾸는 비효율을 제거하려고 사용하는 것이다 IntStream이다.

항상 사용할 수 있는 것은 아니고, 데이터 소스가 기본형일때 Stream<Integer> 대신 IntStream 으로 바꿔서 사용하면 좋다. 

 

그리고 IntStream은 숫자와 관련된 유용한 메서드를 Stream<T>보다 더 많이 제공한다.
왜냐하면 Stream<T>는 T가 숫자일지 아닐지 모르기 때문에, 제공할 수 있는 것이 count 정도밖에 없다.

그런데, IntStream은 숫자인지 알고있기 때문에, sum()도 있고, count()도 되고, average()도 되고 등등 메서드들을 더 제공한다.

반응형

'JAVA' 카테고리의 다른 글

스트림의 연산  (0) 2022.11.16
스트림 만들기  (0) 2022.11.15
메서드 참조, 생성자의 메서드 참조  (0) 2022.08.05
Predicate의 결합, 컬렉션 프레임웍과 함수형 인터페이스  (0) 2022.08.04
java.util.function 패키지  (0) 2022.08.01