DEV/Java

[java] 스트림을 사용하자(1 / 3) - 스트림의 이해와 생성

Bi3a 2023. 9. 21. 18:36

반응형

스트림의 이해와 생성
java 기초 깨부시기

 

    Stream API : 이론

    Stream API : JDK 8 때 도입되어 객체지향적 언어인 Java를 함수형으로 프로그래밍할 수 있게끔 만들어주는 API입니다.
    * JDK8에 도입된 기능 : Stream API, 람다식, 함수형 인터페이스 

     

    Stream의 특징

    1.  원본의 데이터를 조회하며 복사하는 형태로 처리하기 때문에 원본을 수정하지 않습니다.
    2.  일회용입니다. (한번 사용시 파기)
    3.  Stream 내부에서 연산을 반복처리합니다.

     

    Stream 장점

    1. 코드 작성이 간결해지고 효율적입니다. (이해만 되면 말이지)
    2. 코드의 가독성이 상승합니다. (이해만 되면 말이지)
    3. 코드의 재사용성이 높아집니다.

     

    Stream 단점

    1. Stream을 생성하여 처리하는 과정이 보통의 for-loop보다 처리능력이 떨어집니다. (데이터 복사, 객체 생성 후 처리)
    2. 남발하면 오히려 코드가 더러워집니다.
    3. primitive type의 경우는 stream보다 for-loop의 처리속도가 훨씬 빠릅니다. (최적화의 문제)

     

    Stream, 이럴때 사용하면 좋다!

    1. 처리해야 하는 데이터 원소들을 일정한 조건에 맞춰 일괄 처리하는 경우
      1. 일관되게 변환해야 하는 경우
      2. 일정한 조건에 맞춰 삭제, 혹은 필터링해야 하는 경우
      3. 특정 조건에 맞는 원소를 탐색하는 경우.. 등
    2. 적당한 데이터 크기의 코드를 가독성 향상을 위해 리팩터링 하는 경우
      1. 스트림에서 행해지는 파이프라인 연산 처리속도를 고려하고 시행
      2. 스트림에서 연산 속도를 떨어뜨리는 일부 메소드에 주의
      3. 스트림의 파이프라인의 중간 연산을 확인할 수 없으니 주의
    3. 시간복잡도가 충분히 큰 함수를 리팩토링하는 경우
      1. wrapped type의 경우에는 primitive type과 다르게 정적 메모리 'stack'이 아닌 동적 메모리 'heap' 영역에 저장되며므로 비교적 시간복잡도가 높으며, 연산 및 순회 속도가 느립니다. (#그림 1)
      2. primitive type라도 함수 내부의 시간복잡도가 충분히 큰 경우 Stream과 처리 속도에서 유의미한 차이가 없습니다.둘다 무겁고 느리다는 뜻 (#그림 2)

     

    스트림의 성능 비교
    #그림 1 : ArrayList의 모든 원소 순회 시간복잡도: O(n), Stream과 처리 속도에서 큰 차이는 없습니다.

     

    스트림과 유의미한 성능 차이가 없다.
    #그림 2: 함수 내부의 시간 복잡도가 충분히 큰 경우 : Stream과의 유의미한 성능 차이는 없습니다.

     

    Stream의 단계

    Stream의 연산 과정은 단계에 따라 (생성 → 중간처리→ 최종처리)  총 3단계로 구분됩니다. 
    Stream의 연산 과정의 코드를 '파이프라인'이라고 부르는데,
    이는 함수형 프로그래밍으로 구성되어 있는 여러 개의 스트림이 마치 수도관처럼 연결되어 있어 그렇습니다.
    이번 포스팅에서는 Stream의 생성까지만 다루겠습니다.

     

     

    Stream의 생성

    Stream은 일회용으로, 한번 호출 후에 재사용이 불가능함을 유의하며 사용하기 바랍니다.
    해당 Stream의 원소를 확인하기 위해서 본 포스팅에서는 ForEach(System.out::println)을 사용합니다.
      * System.out.println(stream)으로 직접 출력 시 해당 Stream의 주소값을 출력합니다.

     

    String[] arr = {"apple", "banana", "cinamon", "dwain johnson"};
    Stream<String> arrayStream = Arrays.stream(arr);
    arrayStream.forEach(System.out::println);
    arrayStream.forEach(System.out::printf); // 컴파일 에러 : 한번 사용된 Stream은 재사용이 불가능
        // Exception in thread "main" java.lang.IllegalStateException: 
        // 		stream has already been operated upon or closed

     

     

    Array to Stream 변환 : static Arrays.stream(array) 

    static Stream stream(T[] array, *int start, *int end)
    : 제네릭으로 참조타입 T(String, Wrapper 클래스 등)의 배열을 파라미터로 받아서 Stream으로 변환 후 리턴합니다.
    Collection을 상속받은 클래스면 가능합니다.
    *start와 *end 입력 시 해당 start index부터 end -1까지의 index를 리턴합니다.

    일반적으로 초기화되어 있는 배열을 Stream으로 변환할 때 사용합니다.

     

    // (main .. libraries 생략)
    
    // Arrays.stream()은 static 메소드
    String[] arr = {"apple", "banana", "cinamon", "dwain johnson"};
    Stream<String> arrayStream1 = Arrays.stream(arr); // static
    arrayStream1.forEach(System.out::println);
    // out : apple \n banana \n cinamon \n dwain johnson
    
    Stream<String> arrayStream2 = Arrays.stream(arr, 0, 4); // static
    arrayStream2.forEach(System.out::println);
    // out : apple \n banana \n cinamon
    
    // collections.stream()은 non static 메소드
    ArrayList<Integer> integerList = new ArrayList<>(Arrays.asList(1,2,3));
    Stream<Integer> listStream = integerList.stream(); // non static

     

     

    Collections to Stream 변환 : Collections.stream() 

    Collection <T> stream()
    : 배열과 달리 Collection 자료구조는 stream 클래스가 non static으로 구현되어 있습니다.

     

    // (main .. libraries 생략)
    
    ArrayList<Integer> integerList = new ArrayList<>(Arrays.asList(1,2,3));
    Stream<Integer> listStream = integerList.stream();
    listStream.forEach(System.out::println);
    // out : 1 \n 2 \n 3

     

    원시타입 Array to Stream 변환 : Collections.stream() 메서드 활용

    static DoubleStream stream(double[] array)
    static IntStream stream(int[] array)
    static LongStream stream(long[] array)
    : 원시 타입 배열을 오토박싱하지 않고 Stream으로 변환합니다.
    ※ 오토박싱이란 ? :
    Stream API에서 파라미터로 받는 원시형 타입의 값을 참조형 타입으로 자동적으로 바꾸는 처리 과정입니다.
      ex) int(primitive, 원시형) -> Integer(Wrapper, 참조형), char(primitive, 원시형) -> Character(Wrapper, 참조형)

    오토박싱을 통해  int -> Integer로 박싱 하고, 값을 리턴하며 Integer -> int로 다시 언박싱이 진행되기 때문에 그 과정에서 성능 오버헤드가 발생할 수 있습니다.

     

    Stream 클래스에 원시타입 값을 넣으면 오토박싱이 발생합니다.

    따라서 java는 원시 타입에 특화된 Stream 클래스를 제공합니다. (DoubleStream, IntStream, LongStream)

    위 세 클래스는 오토박싱이 일어나지 않습니다.

     

    // (main .. libraries 생략)
    
    int[] intArr = {1, 2, 3, 4, 5};
    double[] doubleArr = {1.1, 2.2, 3.3, 4.4, 5.5};
    
    IntStream intStream = Arrays.stream(intArr); // 오토박싱 X
    intStream.forEach(System.out::println);
    // out : 1 \n 2 \n 3 \n 4 \n 5 \n
    
    DoubleStream doubleStream = Arrays.stream(doubleArr); // 오토박싱 X
    doubleStream.forEach(System.out::println);
    // out : 1.1 \n 2.2 \n 3.3 \n 4.4 \n 5.5 \n

     

    원시타입의 클래스에서  메서드 활용 등의 이유로 참조 클래스로 변환할 때는 boxed()를 통해 직접 박싱이 가능합니다.

     

    // (main .. libraries 생략)
    
    int[] intArr = {1, 2, 3, 4, 5};
    double[] doubleArr = {1.1, 2.2, 3.3, 4.4, 5.5};
    
    IntStream intStream = Arrays.stream(intArr);
    Stream<Integer> IntegerBoxedStream = intStream.boxed(); // 박싱 O
    
    DoubleStream doubleStream = Arrays.stream(doubleArr);
    Stream<Double> DoubleBoxedStream = Arrays.stream(doubleArr).boxed(); // 박싱 O

     

    + Random(), Range()를 활용한 IntStream, LongStream, DoubleStream 생성

    static IntStream.range(start, end)
    : start부터 end-1까지의 값을 stream에 저장합니다.

    static IntStream.rangeClosed(start, end)
    : start부터 end-까지의 값을 stream에 저장합니다.

    static Random(*seed). ints(size),
    static Random(*seed). longs(size),
    static Random(*seed). doubles(size)
    : 각 자료형에 맞는 값을 랜덤 하게 size만큼 산출해 Stream에 저장합니다.
    * seed 값을 넣을 시 각 시드에 맞는 랜덤값을 산출하며, 시드값이 동일할 시 동일한 랜덤값을 산출합니다.

     

    // (main .. libraries 생략)
    
    // intStream range
    IntStream is1 = IntStream.range(1, 10);
    is1.forEach(System.out::print);
    System.out.println();
        // out : 123456789
    
    // intStream rangeClosed
    IntStream is2 = IntStream.rangeClosed(1, 10);
    is2.forEach(System.out::print);
    System.out.println();
        // out : 12345678910
    
    // random 시드 테스트 시작
    IntStream isRandom1 = new Random(3).ints(3);
    isRandom1.forEach(System.out::println);
    System.out.println();
        // out : -1155099828 \n -1879439976 \n 304908421
    
    IntStream isRandom2 = new Random(3).ints(3);
    isRandom2.forEach(System.out::println);
    System.out.println();
        // out : -1155099828 \n -1879439976 \n 304908421
    	// 결론 : 시드가 동일할 시 동일한 난수값 생성
    // random 시드 테스트 끝
    
    // doubleStream Random
    DoubleStream ds = new Random().doubles(3);
    ds.forEach(System.out::println);
    	// out : 0.7347510661407919 \n 0.6194794197458512 \n0.9799304911467713
    
    // LongStream random
    LongStream ls = new Random().longs(3);
    ls.forEach(System.out::println);
    // out : 3949307278875437018 \n 4955889203727499152 \n-5332150430358882651

     

    Stream 생성 후 직접 값을 넣기

    Stream.Of()을 사용하는 법

    // (main .. libraries 생략)
    
    Stream<String> stream = Stream.of("Apple", "Banana", "Cinnamon");
    stream.forEach(System.out::println);
    // out : Apple \n Banana \n Cinnamon

     

    Stream.Builder()을 사용하는 법

    // (main .. libraries 생략)
    
    Stream<String> stream = Stream.<String>builder()
        .add("BTS")
        .add("봉준호")
        .add("손흥민")
        .add("Jay Park")
        .build();
    stream.forEach(System.out::println);
    // out : BTS \n 봉준호 \n 손흥민 \n Jay Park

     

    Stream.empty()를 사용해 빈 객체 생성

    // (main .. libraries 생략)
    
    Stream<String> stream = Stream.empty();
    stream.forEach(System.out::println);

     

    Stream.generator()을 사용하는 법

    전달인자로 함수를 받아 그 반환값으로 이루어진 새로운 스트림을 반환합니다.

    혹은 전달인자를 void로 설정하여 반복된 값으로 이루어진 데이터의 스트림을 생성하는 데 용이합니다.

    limit 값을 통해 생성할 데이터의 maxSize를 설정할 수 있습니다.

    // (main .. libraries 생략)
    
    Stream<String> stream1 = Stream.generate(()->"repeat").limit(5);
    stream1.forEach(System.out::println);
    // out : repeat \n repeat \n repeat \n repeat \n repeat \n repeat
    
    IntStream stream2 = IntStream.generate(()-> 0).limit(3);
    stream2.forEach(System.out::println);
    // out : 0 0 0

     

    Stream.iterate()을 사용하는 법

    초기 값과 초기 값을 가공할 방법을 함수로 받아 그 반환값으로 이루어진 새로운 스트림을 반환합니다.

    초기 값과 반복 조건에 따른 규칙적인 값을 생성하는 데 용이합니다.

    limit 값을 통해 생성할 데이터의 maxSize를 설정할 수 있습니다.

    // (main .. libraries 생략)
    
    Stream<String> stream1 = Stream.iterate(("hello"), str -> str + "o").limit(5);
    stream1.forEach(System.out::println);
    // out : hello \n helloo \n hellooo \n helloooo \n hellooooo
    IntStream stream2 = IntStream.iterate((0), val -> val + 3).limit(3);
    stream2.forEach(System.out::println);
    // out : 0 \n 3 \n 6

     

    이번 시간에는 스트림 파이프라인의 3단계인 생성, 중간처리, 최종처리 중 생성에 대해 알아보았습니다.

    다음 시간에는 람다식을 포함한 스트림의 중간처리 단계에 대해 알아보겠습니다.

     

     

    # REFERENCE

     

    [Java] 스트림(Stream)의 생성

    스트림(Stream)의 생성 1. Stream.of()를 사용하는 방법 Stream.of()로 생성하려는 객체를 입력하면, 새로운 스트림을 반환합니다. public static void createStreamOf() { Stream stream = Stream.of("Python", "C", "Java", "C++", "

    tychejin.tistory.com

     

    [Java] Stream을 생성하는 다양한 방법

    Arrays.stream을 이용하여 배열을 스트림으로 변환제네릭으로 참조타입 T의 배열을 파라미터로 받아서 Stream으로 변환 후 리턴 static Stream stream(T\[] array)유사 매소드 static Stream stream(T\[] ar

    velog.io

     

    java.util.stream (Java Platform SE 8 )

    Interface Summary  Interface Description BaseStream > Base interface for streams, which are sequences of elements supporting sequential and parallel aggregate operations. Collector A mutable reduction operation that accumulates input elements into a mutab

    docs.oracle.com

     

    반응형