본문 바로가기

TIL

[Java]스트림(Stream)

728x90

학습목표

  • 스트림의 특징과 사용 목적을 이해할 수 있다.

스트림은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자이다.

스트림을 사용하면 List,Set,Map,배열 등 다양한 데이터 소스로부터 스트림을 만들 수 있고, 이를 표준화된 방법으로 다룰수 있다.

스트림은 데이터 소스를 다루는 풍부한 메서드를 제공한다. 이를 활용하면 다량의 데이터에 복잡한 연산을 수행하면서도, 가독성과 재사용성이 높은 코드를 작성할 수 있다.

선언형으로 데이터 소스를 처리한다.

스트림을 이용하면 선언형으로 데이터 소스를 처리할 수 있다.

선언형 프로그래밍이란 "어떻게"수행하는지보다는 "무엇을"수행하는 지에 관심을 두는 프로그래밍 패러다임이다.

람다식으로 요소 처리 코드를 제공한다

Stream이 제공하는 대부분의 요소 처리 메서드는 함수형 인터페이스 매개 타입을 가지기 때문에 람다식 또는 메서드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있다.

내부 반복자를 사용하므로 병렬 처리가 쉽다.

외부반복자(external iterator)란 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴을 말한다.

index를 사용하는 for문, Iterator를 이용하는 while문은 모두 외부 반복자를 이용한 형태다.

내부반복자(internal iterator)는 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴을 얘기한다.

내부 반복자를 사용해서 얻는 이점은 컬렉션 내부에서 어떻게 요소를 반복시킬 것인가는 컬렉션에 맡겨두고, 개발자는 요소 처리 코드에만 집중할 수 있다는 점이다.

내부 반복자는 요소들의 반복 순서를 변경하거나 멀티 코어 CPU를 최대한 활용하기 위해 요소들을 분배시켜 병렬 작업을 할 수 있게 도와주기 때문에 하나씩 처리하는 순차적 외부 반복자보다 효율적으로 요소를 반복시킬 수 있습니다.(병렬 스트림을 사용하기 위해서는 스트림의 parallel()메서드를 사용하면 된다.)

Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만 스트림은 람다식으로 요소 처리 내용만 전달할 뿐 반복은 컬렉션 내부에서 일어난다.

병렬처리란 한가지 작업을 서브 작업으로 나누고 서브작업들을 분리된 스레드에서 병렬적으로 처리하는것을 의마한다.

 

중간연산과 최종연산은 할 수 있다.

스트림은 컬렉션의 요소에 대해 중간 연산과 최종 연산을 수행할 수 있는데, 중간 연산에서는 매핑,필터링, 정렬등을 수행하고 최종 연산에서는 반복, 카운팅,평균,총합 등의 집계를 수행할 수 있다.

파이프라인 구성(.)

대량의 데이터를 가공해서 축소하는 것을 일반적으로 리덕션(Reduction)이라고 한다.

데이터의 합계, 평균값,카운팅,최대값,최소값 등이 대표적인 리덕션의 결과물이라 볼 수 있다. 그러나 컬렉션의 요소를 리덕션의 결과물로 바로 집계할 수 없을땐 필터,매핑,정렬,그룹화 등의 중간 연산이 필요하다.

파이프라인

스트림은 데이터의 필터링 ,매핑,정렬,그루핑 등의 중간 연산과 합계,평균,카운팅,최대/최소값 등의 최종 연산을 파이프라인으로 해결합니다. 파이프라인은 여러개의 스트림이 연결되어 있는 구조를 의미하며 최종 연산을 제외하고는 모두 중간 연산 스트림이다.

중간 스트림이 생성될 때 요소들이 바로 중간 연산(필터링,매핑,정렬)되는것이 아니라 최종 연산이 시작되기 전까지는 지연이 된다.

최종 연산이 시작되면 컬렉션의 요소가 하나씩 중간 스트림에서 연산되고 최종 연산까지 오게된다.

stream인터페이스에는 필터링,매핑,정렬 등의 많은 중간 연산 메소드가 있는데 , 이 메소드들은 중간 연산된 스트림을 리턴한다. 그리고 이 스트림에서 다시 중간 연산 메소드를 호출해서 파이프라인을 형성하게 된다.

예를들어 회원 컬렉션에서 남자만 필터링하고 중간 스트림을 연결하고, 다시 나이로 매핑하는 스트림을 연결한 후 최종적으로 평균 나이를 집계한다면 다음처럼 파이프라인이 형성된다.

 

  • 컬렉션과 배열로부터 스트림을 만들 수 있다.

스트림생성, 중간연산, 최종연산

스트림은 본래 "흐름","연속"을 의미한다 자바의 스트림 또한 데이터를 연속적으로 전달하는 통로로 이해할 수 있다

컬렉션 또는 배열에서 스트림을 생성하고,중간 연산을 거쳐 최종 연산에 도달하는 과정을 살펴보자

스트림생성

Collection 인터페이스에는 stream()이 정의되어 있기 때문에, Collection인터페이스를 구현한 객체들(List,Set등)은 모두 이 메서드를 이용해 스트림을 생성할 수 있다.

stream()을 사용하면 해당 Collection의 객체를 소스로 하면 Stream을 반환한다.

배열의 원소들을 소스로하는 Stream을 생성하기 위해서는 Stream의 of메서드 또는 Array의 stream메서드를 사용한다.

객체를 위한 Stream외에도 int와 long 그리고 double과 같은 원시 자료형들을 사용하기 위한 특수한 종류의 Stream(intStream,LongStream,DoubleStream)들도 사용할 수 있으며,Instream은 range()함수를 사용하여  기존의 for문을 대처 할 수 있습니다.

스트림은 주로 컬렉션과 배열에서 얻지만 다음과 같은 소스로부터 스트림 구현 객체를 얻을 수도 있다.

리턴                                             타입메서드(매개 변수)                                                                                                          소스
Stream java.util.Collection.Stream(), java.util.Collection.parallelSream( ) 컬렉션
Stream, IntStream, LongStream, DoubleStream Arrays.stream(T[]), Arrays.stream(int[]), Arrays.stream(long[]), Arrays.stream(double[]), Stream.of(T[]), IntStream.of(int[]) LongStream.of(long[]), DoubleStream.of(double[]) 배열
IntStream IntStream.range(int, int), IntStream.rangeClosed(int, int) int 범위
LongStream LongStream.range(long, long), LongStream.rangeClosed(long, long) long 범위

스트림을 사용할 때 주의할점

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

스트림은 일회용이여서 한번 사용하면 닫히므로 필요하면 새로운 스트림을 만들어야한다.

 

중간연산

중간 연산은 연산 결과를 스트림으로 반환하기 때문에, 연속해서 여러번 수행 할 수 있다.

필터링(Filter(),Distinct())

distinct() : Stream의 요소들에 중복된 데이터가 존재하는 경우, 중복을 제거하기 위해 사용된다.

filter() : Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 만들어 냅니다. 필터 메서드에는 매개값으로 조건(Predicate)이 주어지고, 조건이 참이 되는 요소만 필터링 합니다.

매핑(map())

기존의 Stream요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산입니다.

저장된 값을 특정한 형태로 변환하는데 주로 사용되며, Java에서는 map함수의 인자로 함수형 인터페이스function을 받고있습니다.

예를들어 String을 요소들로 갖는 Stream을 모두 대문자 String의 요소들로 변환하고자 할 때 map을 이용할 수 있다.

map()이외에도  mapToInt(), mapToLong(), mapToDouble()등의 메서드가있다

map()메서드는 작업을 하다보면 일반적인 Stream객체를 원시 Stream으로 바꾸거나 그 반대로 하는 작업이 필요한 경우 쓰인다. 반대로 원시 객체는 mapToObject를 통해 일반적인 Stream객체로 바꿀수 있다

flatMap() : 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴한다.

flatMap()과 map()의 차이점은, map()은 스트림의 스트림을 반환하는 반면, flatMap()은 스트림을 반환한다는것이다.

예를 들면, 전자의 리턴 타입이 Stream<Stream>이라면, 후자의 리턴 타입은 Stream이 되는것이다.

 

정렬(sorted())

Stream의 요소들을 정렬하기 위해서는 sorted를 사용해야 하며, 파라미터로 Comparator를 넘길 수도 있다.

Comparator 인자 없이 호출할 경우에는 오름차순으로 정렬이 되며, 내림차순으로 정렬하기 위해서는 Comparator의 reverseOrder를 이용합니다.

 

연산 결과 확인(peek())

peek(), forEach()는 요소를 하나씩 돌면서 출력한다는 기능에서는 동일하지만, 동작 방식은 다릅니다.

peek은 중간 연산 메서드이고, forEach는 최종 연산 메서드입니다.

forEach는 스트림의 요소를 소모하므로 한 번만 호출할 수 있지만(재호출하려면 새로운 스트림을 생성해야 합니다), peek은 중간 연산이므로 하나의 스트림에 여러 번 사용할 수 있습니다.

peek()은 주로 연산 중간에 결과를 확인하여 디버깅하고자 할 때 사용합니다.

 

최종 연산

연산 결과 확인(forEach())

forEach는 최종 연산 메서드이기 때문에 파이프라인 마지막에서 요소를 하나씩 연산합니다.

forEach값을 출력할 때도 사용하지만, 이메일 발송, 스케줄링 등 리턴 값이 없는 작업에서도 많이 사용합니다.

 

매칭(match())

Stream의 요소들이 특정한 조건을 충족하는지 검사하고 싶은 경우에는 match() 메서드를 이용할 수 있습니다.

match() 메서드는 함수형 인터페이스 Predicate를 받아서 해당 조건을 만족하는지 검사하고, 검사 결과를 boolean으로 반환합니다. match() 메서드에는 크게 다음 3 가지가 있습니다.

 

  • allMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
  • anyMatch() : 최소한 한 개의 요소가 매개값으로 주어진 Predicate의 조건을 만족하는지 조사
  • noneMatch() : 모든 요소들이 매개값으로 주어진 Predicate의 조건을 만족하지 않는지 조사

기본 집계(sum(), count(), average(), max(), min())

집계는 최종 연산 기능으로 요소들을 카운팅, 합계, 평균값, 최대값, 최소값 등으로 연산하여 하나의 값으로 산출하는 것을 의미합니다.

스트림의 주요 메서드를 활용해 필요한 연산을 수행할 수 있다.

reduce()

앞서 본 집계 메서드인 sum, average, count, max, min 외에도 다양한 집계 결과물을 만들 수 있는 reduce 메서드가 있습니다.

reduce는 누적하여 하나로 응축(reduce)하는 방식으로 동작합니다. 앞의 두 요소의 연산 결과를 바탕으로 다음 요소와 연산합니다.

reduce 메서드는 최대 3개의 매개변수를 받을 수 있습니다.

  1. Accumulator: 각 요소를 계산한 중간 결과를 생성하기 위해 사용
  2. Identity: 계산을 수행하기 위한 초기값
  3. Combiner: 병렬 스트림(Parlallel Stream)에서 나누어 계산된 결과를 하나로 합치기 위한 로직


초기값이 있는 reduce는 초기값과 스트림의 첫 번째 요소로 첫 연산을 수행합니다. 초기값이 없는 reduce는 스트림의 첫 번째 요소와 두 번째 요소로 첫 연산을 수행합니다.

sum1의 결과가 나오는 코드는 스트림에 요소가 없을 경우, NoSuchElementException이 발생하지만, sum2의 결과가 나오는 코드는 디폴트 값(identity)인 0을 리턴합니다. 스트림에 요소가 있을 경우는 모두 동일한 결과를 산출합니다.

앞서 살펴본 count()와 sum() 등 집계 메서드는 내부적으로 모두 reduce()를 사용합니다.

collect()

Stream의 요소들을 List나 Set, Map, 등 다른 종류의 결과로 수집하고 싶은 경우에는 collect 메서드를 이용할 수 있습니다. collect 메서드는 어떻게 Stream의 요소들을 수집할 것인가를 정의한 Collector 타입을 인자로 받습니다. 이는 Collector 인터페이스를 구현한 클래스입니다.

일반적으로 List로 Stream의 요소들을 수집하는 경우가 많습니다. 자주 사용하는 작업은 Collectors 객체에서 static 메서드로 제공하고 있고, 원하는 것이 없는 경우에는 Collector 인터페이스를 직접 구현하여 사용할 수도 있습니다.

Optional<T>
NullPointerException(NPE), 즉 null값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었다.
연산결과를 Optional에 담아서 반환하면, 따로 조건문을 작성해주지않아도 NPE가 발생하지 않도록 코드를 작성할수있다.
Optional클래스는 모든 타입의 객체를 담을수있는 래퍼 클래스다.
Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용한다 참조변수의 값이null일 가능성이 있다면 ofNullable()을사용
Optional 타입의 참조변수를 기본값으로 초기화 하려면 empty()메서드를 사용한다.

Optional 객체에 객체에 저장된 값을 가져오려면 get()을 사용하고

참조변수의 값이 null일 가능성이 있다면 orElse()메서드를 사용해 디폴트 값을 지정할 수 있다.

Optional 객체는 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있습니다(이를 메서드 체이닝이라고 합니다).

Buffer > 임시저장공간

스트림(물의 흐름) - 물이 흘러 수도관을 타고 흘러감 

컴퓨터 키보드에 a를 치면 컴퓨터로 숫자 65(10진수)로 변경해서 전송이됨

컴퓨터입장에선 a(65)라는 데이터가 들어옴 (input) 

컴퓨터가 모니터로 a(65)라는 데이터를 내보냄 (output) 아스키코드 표를 보고 분석이 가능함

2진수와 10진수의 차이

박스에 몇개가 들어가느냐의 차이 2진수는 0과1만, 10진수는 0~9까지들어감

 

728x90

'TIL' 카테고리의 다른 글

스레드(Thread)  (1) 2022.09.16
[Java]추상화(Abstraction)  (0) 2022.09.16
[Java]람다식(Lambda Expression)  (0) 2022.09.15
[Java]애너테이션(Annotation)  (0) 2022.09.15
[Java] 다형성(Polymorphism)  (0) 2022.09.14