코드스테이츠_국비교육/[Section1]

18.02_스트림_22.09.15

생각없이 해도 생각보다 좋다. 2022. 9. 17. 11:59

학습 키워드

  • 스트림
  • 스트림의 활용
  • 스트림의 주요 메서드

 

스트림, Stream

>의미

: 데이터 소스를 가공하여 결과값을 도출하는 흐름

: 데이터 소스의 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자

그림 01. Stream (Pipe model)

>특징

  • 1. 선언형 프로그래밍을 할 수 있다.

: 프로그램이 '어떤 방법으로 해야 하는지'보다는 '무엇을 수행하는지'를 설명하는 프로그래밍

: 즉, 문제 처리의 과정인 알고리즘을 보여주는 것이 아니라, 프로그램의 목표를 알려주는 프로그래밍

  • 2. 요소 처리 코드를 람다식으로 표현할 수 있다.
  • 3. 중간 연산과 최종 연산을 수행한다.

: 지연된 연산; 최종 연산이 실행될 때 까지 연산이 지연된다.

: 중간 연산의 반환값은 Stream이고, 최종 연산의 반환값은 Stream이 아닌 자료형의 값이다. (연산이 지연된다는 의미

의 이유인 것 같음)

  • 4. 원본을 변경하지 않는다. (READ-ONLY)
  • 5. 병렬 처리가 쉽다

: 내부 반복자를 사용할 수 있기 때문에

: 멀티 코어 CPU를 효율적으로 활용할 수 있다. (내부 데이터를 바꾸지 않기 때문에 병렬 처리가 가능한 것 같다)

 

/*

외부 반복자와 내부 반복자

외부 반복자

: 개발자가 코드로 직접 요소를 반복해서 가져오는 것

: for, while, Iterator 들이 외부 반복자를 이용하는 것

내부 반복자

: 개발자는 요소당 처리할 코드만 제공하고 반복은 컬렉션 내부에서 진행하는 것.

: 아직 확실한 이해는 못했지만, 외부 반복자는 값을 직접 가져와서 사용하는 코드이고, 내부 반복자는 사람의 눈으로는 읽

기만 하는 코드로 볼 수 있다.

: 콜렉션 내부에서 반복을 처리한다는 의미가 곧 콜레션을 변경하지 않고 연산을 수행하는 것을 의미하는 것 같다.

*/

 

스트림의 활용

>스트림 사용 단계 

  1. 스트림 생성
  2. 중간 연산
  3. 최종 연산

//각 단계에 해당하는 메소드들이 많다. 자주 쓰는 것은 외우고, 필요할 때마다 검색하면 될 듯.

//각 단계 메서드 정리

https://hso8706.tistory.com/62

>스트림 생성

: 파이프로 흘려보낼 데이터를 일렬로 정렬시키는 과정

: 스트림 생성 메서드를 사용하면 다양한 데이터 소스들을 Stream의 객체(인스턴스)로 반환시킨다.

: 데이터 소스들을 인스턴스로 만든 것이기 때문에 실제 값은 변하지 않고 읽기만 하는 것이다.

: IntStream, LongStream, DoubleStream과 같은 기본 자료형을 위한 특수한 종류의 Stream이 존재한다.

: 각각의 반환 타입마다 메서드가 다르기 때문에 적절하게 써주는 것이 좋다.

: 또한, 기본 자료형을 다룰 경우 위의 특수 Stream을 써주면 불필요한 과정(예: 박싱, 언박싱)을 안할 수 있으므로 좀 더 효율적이다.

>스트림 중간 연산

: 생성한 스트림을 필터링하는 과정(가공하는 과정)

: 다양한 메서드를 통해 Stream 인스턴스로 반환

: 최종 연산까지 필요한 만큼 중간 연산을 해도 되고, 중간 연산을 생략해도 된다.

: 중간 연산의 순서는 상관없다. (값은 달라질 수도 있지만)

>스트림 최종 연산

: 필터링된 스트림(가공된 스트림)을 사용할 수 있는 데이터로 만드는 과정.

: Stream이 아닌 자료형의 값으로 반환

: 정확히는 값 또는 현상으로 반환

: 최종 연산은 마지막 단계에서 딱 한 번만 가능함.

: 최종 연산 이후에 스트림은 사라짐, 재사용 불가. (인스턴스)

 

>스트림 코드 예시

1. 기본

public class Main {
    public static void main(String[] args) {
        //데이터 소스; List<Stream> 타입;
        List<String> list = Arrays.asList("A", "B", "C", "D", "E", "F");
        //1. Stream 생성; Collection.stream() 메서드 이용; 요소 타입<String>; 반환 타입 Stream<String>
        Stream<String> listStream = list.stream();
        //2-1. 중간 연산1; pipe1; "B" 제거; 요소 타입<String>; 반환 타입 Stream<String>
        Stream<String> pipe1 =listStream.filter(n -> !n.equals("B"));
        //2-2. 중간 연산2; pipe2; "D", "F" 제거; 요소 타입<String>; 반환 타입 Stream<String>
        Stream<String> pipe2 =pipe1.filter(n -> !n.equals("D") && !n.equals("F"));
        //3. 최종 연산; Collectors.toList()메서드 이용; List<String>으로 반환
        List<String> result = pipe2.collect(Collectors.toList());

        System.out.println(result);
        /*
        [A, C, E]
         */
    }
}

2. 메서드 체이닝 이용

public class Main {
    public static void main(String[] args) {
        //데이터 소스; List<Stream> 타입;
        List<String> list = Arrays.asList("A", "B", "C", "D", "E", "F");
        //메서드 체이닝 이용
        //최종 결과의 참조형과 참조변수에 모든 Stream처리 과정을 작성.
        //문장이 길 경우, 개행과 들여쓰기로 표현(가독성)
        //최종 연산 후에 세미콜론(;) 사용
        List<String> result = list.stream()
                .filter(n -> !n.equals("B"))
                .filter(n -> !n.equals("D") && !n.equals("F"))
                .collect(Collectors.toList());

        System.out.println(result);
        /*
        [A, C, E]
         */
    }
}

Stream 공식 문서
https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html


Optional<T>

>의미

: Null 값으로 인해 에러가 발생하는 현상을 방지하고, 이를 위해 사용하는 객체

: NullPointerException(NPE)

: 모든 타입의 객체를 담을 수 있는 Wrapper 클래스

: 다양한 메서드가 존재하고 이는 Stream의 메서드와 유사

>사용

: 메서드 체이닝(여러 메서드를 연결함)을 통해 스트림처럼 사용할 수 있음.

: Stream 객체를 생성하는 것 대신, Optional<T>의 객체로 데이터를 리스트화해서 사용. (스트림 생성하듯)

: Stream의 최종 연산 메서드의 반환값으로 사용.

 

//스트림을 사용할 때도 중간 연산에 Null을 방지하는 조건을 넣어주거나, try-catch문으로 잡거나, 맨처음 If로 엣지 잡아주

면 되긴 할텐데... Optional<T>의 사용은 취향일까? 아님 필요한 곳이 꼭 있을까? 편한지는 좀 써봐야 알 것 같다.

//우선은 있는 것만 알아두고, 필요할 때 깊게 공부하자.

Optional 공식 문서
https://docs.oracle.com/javase/8/docs/api/java/util/Optional.html