[Java & Kotlin] Stream

2023. 10. 9. 16:54·JVM/Java

Stream 은 왜 만들어졌나?

Java , Kotlin 은 List, Set, Array 등 다양한 Collection 을 제공한다.

Collection API 와 Iterate 방식은 표준화 되었지만

각 클래스는 같은 기능을 하는 메소드가 중복 정의되어 있다.

 

List 를 정렬하는 방법만 해도 쉽게 2가지를 떠올릴 수 있다.

  1. List.sort
  2. Collection.sort

리스트 정렬 

public class JavaStreamTest {
  @DisplayName("Sort list  with two way ")
  @Test
  void forLoop() {
    List<Integer> nums = Arrays.asList(5, 4, 3, 2, 1);

    nums.sort(new Comparator<Integer>() {
      @Override
      public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
      }
    });

    for (Integer num : nums) {
      System.out.print(num);
    }

    System.out.println();
    Collections.sort(nums, new Comparator<Integer>() {
      @Override
      public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
      }
    });

    for (Integer num : nums) {
      System.out.print(num);
    }
  }
}

 

 

배열은 또 Arrays.sort() 를 사용한다.

배열 정렬

public class JavaStreamTest {
  @DisplayName("Sort array using Arrays.sort")
  @Test
  void arraySort() {
    int[] arr = new int[]{1,2,3,4,5};
    Arrays.sort(arr);
  }
}

 

 

정렬 API가 중복 정의되어 있다.
Arrays.sort(), Collections.sort(), List.sort()

 

스트림이 필요한 이유

List.sort, Collection.sort, Arrays.sort() 모두 같은 기능을 하는 메소드가 중복 정의되어있다.

여러 메소드를 산발적으로 사용하면, 코드를 읽는 개발자 입장에서 의미를 파악하는데 인지 부하가 증가한다.

스트림은 이 문제를 해결하기 위해 등장했다.

스트림은 데이터 소스와 관계 없이 같은 방식으로 API 를 표준화했다.

이로써 코드 가독성 및 재사용성을 높인다.

public class JavaStreamTest {
  @DisplayName("Sort list using stream")
  @Test
  void streamSort() {
    int[] arr = new int[]{5,4,3,2,1};
    List<Integer> numList = Arrays.asList(5, 4, 3, 2, 1);

    Arrays.stream(arr).sorted(); // array , list 모두 stream화 후 같은 API 사용
    numList.stream().sorted();
  }
}

 

Stream 핵심 특징

  • 최종 연산 전까지 중간 연산이 실행되지 않는다. - Lazy Evaluation
  • 데이터 소스 (원본)를 수정하지 않는다. - Immutable
  • 일회용이다.
  • 병렬 처리를 지원한다. - parallel()

 

Lazy Evaluation vs Eager Evaluation

Lazy Evaluation

지연 평가(Lazy Evaluation) 은 실제 계산이 필요한 시점까지 계산을 미루는 것을 의미한다.

값을 계산하는 시점을 늦추어 불필요한 계산을 방지하여 성능 향상을 도모한다.

class EvaluationTest {
	@DisplayName("Java Stream evaluate lazy")
    @Test
    fun lazyEval() {
        // Java Stream doesn't return stream result until final operation

        // given
        val intStream = IntStream.rangeClosed(1, 9) // [1,2,3,4,5,6,7,8,9]

        // when
        val doubledStream = intStream
            .filter { // 중간 연산
                println("filter: $it")
                it <= 3
            }
            .map { // 중간 연산
                println("map: $it")
                it * 2
            }

		// 최종 연산 실행 전에 메시지가 출력된다.
        println("Not yet execute final operation")
        // anyMatch meets '4' not filter and map operations execute '3 ~ 9'

        val result = doubledStream.anyMatch { it >= 4 } // 최종 연산
        // then
        assertThat(result).isTrue()
    }
}

anyMatch 조건에 해당하는 숫자 2가 나오자 이후 숫자 연산은 생략했다.

filter, map 연산이 '2'까지만 실행된다. 

3~9 등 불필요한 연산을 생략하는 이 전략을 바로 Lazy Evaluation 이라한다.

 

Eager Evaluation

조급한 평가 (Eager Evaluation)은 모든 계산을 바로 실행하겨 결과 값을 도출하고 다음 계산으로 넘아가는 방식을 의미한다.

즉, 각 중간 연산을 실행하고 다음 중간 연산으로 넘어간다.

 

Kotlin Collection 은 기본적으로 eager evaluation 방식으로 동작한다.

 

class EvaluationTest {
    @DisplayName("Kotlin Collections evaluate eager")
    @Test
    fun eagerEval() {
        // Kotlin Collections return collection each step (eager evaluation)

        // given
        val intArray = IntArray(9){it + 1}
        // when
        val doubledCollection = intArray
            .filter { // 중간 연산
                println("filter: $it")
                it <= 3
            }
            .map { // 중간 연산
                println("map: $it")
                it * 2
            }

        println("Not yet execute final operation")

        val result = doubledCollection.any { it >= 4 } // 최종 연산

        // then
        assertThat(result).isTrue()
    }
}

filter 와 map이 모두 실행된다.

최종 연산 도달 전에 모든 중간 연산을 바로 실행한다.

모든 원소에 대해 연산이 실행되기  때문에 데이터 양이 많아지면 Lazy Evaluation 에 비해 성능 저하가 있을 수 있다.

 

중간 연산 vs 최종 연산

중간 연산

연산 결과가 스트림인 연산.

체이닝 기법으로 메소드 연쇄 호출로 자주 사용된다.

 

  • map
  • flatMap
  • distinct
  • skip
  • peek
  • sorted

 

최종 연산

연산 결과가 스트림이 아닌 연산.

스트림 요소를 소모하여 단 한번만 가능하다.

  • forEach
  • count
  • findAny, findFirst
  • allMatch, anyMatch, noneMatch
  • toArray, toList 
  • reduce
  • collect

 

class StreamIntermediateOperation {
	@DisplayName("Streams can extract or transform using map and filter")
    @Test
    fun mapFilter() {
        // given
        val fileStream = Stream.of(
            File("Ex1.jpg"),
            File("Ex2.jpg"),
            File("Ex3.png"),
            File("Ex4")
        )
        // when
        val fileNameList = fileStream
            .map { it.name } // 중간 연산
            .filter { it.indexOf(".") != -1 } // 중간 연산
            .map { it.uppercase(Locale.getDefault()) } // 중간 연산
            .toList() // 최종 연산

        // then
        assertThat(fileNameList).isEqualTo(listOf(
            "EX1.JPG",
            "EX2.JPG",
            "EX3.PNG")
        )
    }
}

 

 

 

스트림은 일회용이다.

스트림은 한번 소모(Consume) 되면 재사용이 불가능하다.

class StreamConstruction {
	@DisplayName("Stream is allowed to use once")
    @Test
    fun streamOnlyOnce() {
        val list = listOf(1,2,3,4,5)
        val intStream: Stream<Int> = list.stream()
        // when then
        intStream.forEach(System.out::print)
        // then
        val errMessage = assertThrows<IllegalStateException> {
            intStream.forEach(System.out::print)
        }.message

        assertThat(errMessage).isEqualTo("stream has already been operated upon or closed")
    }
}

 

forEach() 는 최종 연산으로써 스트림을 소모한다.

이미 소모된 스트림을 재사용하려하자 'IllegalStateException' 이 발생한다.

 

 

저작자표시 (새창열림)

'JVM > Java' 카테고리의 다른 글

[객체지향] 잘못된 DRY 원칙 적용  (0) 2023.12.16
[Java] Enum 에는 equals 대신 == 을 써라  (0) 2023.11.22
[Java] SpringBoot 없이 Yaml config 로드하기 (feat.SnakeYaml)  (0) 2023.09.15
Presentation - Business DTO를 분리시켜라  (0) 2023.08.06
[Java] Data Transfer Object (DTO)  (0) 2023.01.27
'JVM/Java' 카테고리의 다른 글
  • [객체지향] 잘못된 DRY 원칙 적용
  • [Java] Enum 에는 equals 대신 == 을 써라
  • [Java] SpringBoot 없이 Yaml config 로드하기 (feat.SnakeYaml)
  • Presentation - Business DTO를 분리시켜라
M_Falcon
M_Falcon
  • M_Falcon
    Falcon
    M_Falcon
  • 전체
    오늘
    어제
    • 분류 전체보기 (429)
      • Web (16)
        • Nodejs (14)
        • Javascript (23)
        • FrontEnd (4)
      • DataBase (39)
        • Fundamental (1)
        • Redis (4)
        • PostgreSQL (10)
        • NoSQL (4)
        • MySQL (9)
        • MSSQL (3)
        • Error (4)
      • Algorithm (79)
        • Algorithm (문제풀이) (56)
        • Algorithm (이론) (23)
      • JVM (64)
        • Spring (13)
        • JPA (5)
        • Kotlin (13)
        • Java (23)
        • Error (7)
      • 기타 (68)
        • Kafka (3)
        • Kubernetes (3)
        • Docker (12)
        • git (19)
        • 잡동사니 (26)
      • 재테크 (11)
        • 세무 (4)
        • 투자 (3)
        • 보험 (0)
      • BlockChain (2)
        • BitCoin (0)
      • C (32)
        • C (10)
        • C++ (17)
        • Error (3)
      • Low Level (8)
        • OS (3)
        • 시스템 보안 (5)
      • 네트워크 (3)
      • LINUX (30)
        • Linux (26)
        • Error (4)
      • 저작권과 스마트폰의 이해 (0)
      • 생각 뭉치 (6)
      • 궁금증 (2)
      • Private (4)
        • 이직 경험 (0)
        • 꿈을 찾아서 (1)
      • Android (21)
        • OS (4)
  • 블로그 메뉴

    • 홈
    • WEB
    • 알고리즘
    • DataBase
    • Linux
    • Mobile
    • C
    • 방명록
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    백준
    android
    Programmers
    Git
    Spring
    docker
    linux
    kafka
    algorithm
    프로그래머스
    Bitcoin
    database
    Kotlin
    알고리즘
    PostgreSQL
    JPA
    java
    ubuntu
    javascript
    C++
  • hELLO· Designed By정상우.v4.10.3
M_Falcon
[Java & Kotlin] Stream
상단으로

티스토리툴바