ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Stream 정리 및 예제
    java 2021. 5. 29. 10:50

    배경

    https://stackify.com/streams-guide-java-8/

    위 글 변역하며 공부

     

     

    Introduction

    stream은 datasource의 래퍼클래스이며 대용량 처리를 간편하고 빠르게 작업하도록 도와준다. stream은 데이터를 저장하지 않으므로 자료구조라고 보는 건 부적절하며 원본datasource를 변경하지 않는다.

     

     

    Stream Creation

    of() 외에도 스트림을 만드는 다양한 방법이 있고 배열이 아니더라도 컬렉션을 통해 스트림을 만들 수 있다.

     

     

    Stream Operation

    forEach

    forEach()는 하나의 스트림을 소모하면서 그에 맞는 action을 수행하므로 Cunsumer 콜백을 전달하면 된다. forEach()는 종결스트림함수로 호출하면 더이상 새로운 연산을 수행하지 못하게 된다.(뒤에 자세히)

     

    map

    map()은 새로운 스트림을 반환하는 중간연산스트림이다. 새로운 스트림은 다른 반환형을 가져도 된다. Function을 콜백으로 넘긴다.

     

    collect()

    collect()는 스트림을 자료구조로 변환하는 방식을 추상화했다. Collector형 자료구조를 만드는 콜백을 전달하면 알맞게 스트림을 컬렉션으로 변환한다.

     

    filter()

    map()처럼 하나의 스트림을 받지만 반환형은 boolean이다. 즉, Predicable 콜백을 넘기는데 예상한 대로 콜백 값이 true인 경우만 스트림을 살리고 값이 false인 경우 스트림을 버린다.

     

    findFirst()

    첫 결과 스트림만 Optional로 래핑해서 반환한다. 스트림은 기본적트로 하나 씩 끝까지 연산을 수행하므로 최적화한 연산이 가능하다.

     

    toArray()

    컬렉션으로 결과를 반환하는 collect()와 달리 배열 형태로 스트림을 변환한다. 기본적으로 Object[]형태로 반환하지만 적절한 배열 타입 생성자를 콜백으로 넘기면 원하는 타입으로 반환된다. 다만, 스트림의 자료형을 컴파일타임에 체크하지 않아서 런타임에러가 나올 수 있다.

     

    flatMap()

    예제를 확인해보면 어떤 상황에서 쓰일지 예상이 된다. Stream의 제네릭스가 컬렉션과 같이 다수의 데이터를 갖고 있다면 즉, 스트림까지 2차원 구조라면 이를 1차원으로 변환해준다. 동작순서는 하나의 컬렉션스트림을 변환하고 변환한 스트림을 소모 후 다음 스트림을 작업한다. 즉, 1차원으로 전부 변환하고 나머지 작업을 진행하는 게 아니라 하나의 스트림 을 변환하고 변환한 걸 전부 처리한 후 다음 스트림을 변환하는 식이다.(peek()사용해서 확인 가능)

     

    peek()

    진행 중인 스트림을 출력해서 확인할 수 있다. 결과에 영향을 끼치지 않으므로 학습 또는 디버깅용으로 좋다.

     

    스트림 파이프라인

    이처럼 스트림은 중간연산과 최종연산으로 구분할 수 있다. 중간연산은 그 결과가 스트림이고 최종연산은 결과가 스트림이 아니다. 스트림소스부터 중간연산과 최종연산을 합친 하나의 작업을 스트림 파이프라인이라고 한다.

     

    short-circuiting operations

    if( A && B() ) 식에서 A가 true인 경우 B는 메서드가 수행되지 않는다. 이는 boolean의 short-circuiting이라 하는데 stream에서도 몇몇 연산은 이런식으로 동작한다. 이를테면 limit(n)중간연산은 n개의 스트림을 처리한 경우 나머지 스트림은 처리하지 않는다. findFirst() 경우도 첫스트림이 도착하면 나머지 스트림에 대한 연산을 진행하지 않는다.

     

     

    Lazy Evaluation

    지연연산은 스트림의 최적화를 위한 중요한 특징이다. 소스데이터 계산은 최종연산이 초기화 된 경우에만 수행된다. 모든 중간연산은 최종연산이 호출되기 전까지 수행되지 않는다. 또한 스트림 하나가 완료되어야 다음 스트림이 수행되므로 findFirst()와 같은 연산에서 모든 스트림을 작업하지 않아도 된다.

     

     

    비교 연산

    sorted()

    sorted()를 이용하면 중간에 스트림을 모으고 정렬할 수 있다. 최종연산처럼 모아 놓은 스트림을 이용해 작업하면서 결과는 스트림으로 반환하는 게 특징이다.

     

    dintinct()

    스트림에서 중복 요소를 제거하고 반환한다. 중복을 판단할 때 ==가 아닌 equals()를 사용한다.

     

     

    allMatch() anyMatch() noneMatch()

    predicate을 콜백으로 넘기고 false를 반환하면 스트림 진행을 종료한다. 반환형 또한 true or false이다.

     

     

    기본형 스트림

    기본형으로 스트림을 진행한다면 기본형스트림을 사용해볼 수 있다. 연산 수행 중 박싱과 언박싱 작업이 은근히 비용이 들기 때문이다. IntStream은 대부분의 연산이 레퍼런스 타입이 아닌 primitive로 되어 있다. 객체를 담는 컬렉션으로 전환하고 싶은 경우 기본형스트림을 스트림으로 박싱하는 과정이 필요해서 boxed() 메서드를 호출한 걸 확인할 수 있다. 이외에도 Double, Long에 대한 기본형 스트림을 추가적으로 제공한다. 

     

     

    Reduction

    감소연산은 연속된 연산 결과를 하나로 합친 결과를 반환한다. min(), max(), findFirst와 같은 연산을 보았다.

     

    reduce()

    identity라는 초깃값과 순차적으로 수행할 작업을 콜백으로 넘겼다. min() max()와 같은 연산은 내부적으로 reduce()를 이용해 구현한 걸 확인할 수 있다.

     

    그루핑

    partitioningBy

    스트림을 두 파티션으로 나눈다. 두 파티션이기 때문에 key가 true 또는 false인 것을 확인할 수 있다. 직관적으로 어려운 내용은 아닌 것 같다.

     

    groupingBy

    스트림을 여러 파티션으로 나눈다. 파티션의 기준이 될 key값을 정할 콜백을 넘기면 된다. groupingBy에 두번째 매계변수로 mapping을 이용하면 value를 List가 아닌 컬렉션으로 변환할 수 있다. reducing을 이용하면 value에 컬렉션이 아닌 Optional 단일 값을 넣을 수 있다. 일단 있다 정도로 넘어가고 나중에 사용할 일이 있으면 쓰자.(뭐이리 많냐..)

     

     

    Parallel Stream

    위에서 언급했듯이 스트림은 한 요소를 끝까지 작업하고 다음 요소를 작업한다. 이런 파이프라이닝을 병렬로 수행할 수 있게 해주는 게 parallel()메서드다. 내부적으로 fork&join프레임워크를 이용했는데 그런 속사정을 알 필요없이 간단하게 구현한 수 있다. 다만, 말도 많고 탈도 많다고 한다. 주의사항을 체크하고 사용을 심사숙고하자.

     

    - thread-safe임을 보장해야한다. 멀티스레드로 작업을 분할하기 때문에 당연한 이야기다.

    - 연산 순서가 보장되어야하면 사용해서는 안된다. 예를 들어 findFirst()같은 경우는 사용할 수 없다.

    - 성능 개선이 명확할 때 사용하자. 의외로 병렬처리가 성능을 보장하지 못하는 경우가 있다.

     

    Infinite Stream

    generate()

    generate()를 이용하면 무한 스트림을 생성할 수 있다. 이러한 스트림은 반드시 limit와 같이 끝을 보장하는 연산과 함께 수행되어야 한다.

     

    iterate()

    초깃값과 계속해서 생성할 콜백을 넘기면 무한 스트림을 생성한다. 마찬가지로 limit과 같이 종결연산을 넣어서 사용해야 한다.

     

    generate()과 iterate()의 차이는 스트림 생성 시 seed값을 갖고 안 갖고 차이다. seed가 있는 iterate은 콜백이 Function이고 generate은 콜백이 Supplier인 걸 보면 좀 더 이해하기 쉽다^^.

     

     

    마치며

    원래 문법 공부를 하면 주로 사용하거나 사용할 것 같은 기분이 드는 메서드만 주로 공부한다. Optional과 Stream을 이런 방식으로 공부했었고 어떤 계기로 인해 자세히 공부하게 되었다. 깊게 가면 많겠지만 우선 이정도하면 실전에서 사용하고도 남을 정도인 것 같다.

    'java' 카테고리의 다른 글

    Future 클래스 정리  (0) 2021.06.03
    Generics와 wildcard  (0) 2021.06.01
    Optional 정리  (0) 2021.05.28
    Inner class와 static 조합  (0) 2021.05.27
    Effective Java 모든 객체의 공통 메서드  (0) 2021.05.25

    댓글

Designed by Tistory.