주난v 개발 성장기

[자바 8인 액션] 4장. 스트림 소개 본문

개발 성장기/JAVA

[자바 8인 액션] 4장. 스트림 소개

주난v 2020. 7. 5. 20:21

컬렉션은 데이터를 그룹화하고 처리할 수 있다. 프로그래밍 작업에 필수적인 요소다.

하지만, 특정 조건에 대한 처리에는 약하다.

 

많은 요소를 포함하는 커다란 컬렉션은 어떻게 처리할까?

-> 성능을 높이려면 멀티코어 환경에서 병렬로 컬렉션 요소를 처리해야 한다.

     하지만, 병렬 처리 코드를 구현하는 것은 어렵다.

 

따라서, "스트림"을 사용하는 것이 답이다.

 

스트림이란 무엇인가?

- 자바 API에 추가된 기능, 스트림을 이용하면 선언형으로 컬렉션 데이터를 처리할 수 있다.

또한 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.

 

List<Dish> lowCaloricDishes = new ArrayList<>();
for (Dish d : menu) {
	if (d.getCalories() < 400) {
    	lowCaloricDishes.add(d);
    }
}

//자바 8

List<String> lowCaloricDishesName = menu.stream().filter(d -> d.getCalories() < 400)
												 .sorted(comparing(Dish::getCalories))
                                                 .map(Dish::getName)
                                                 .collect(toList());

 

filter(or sorted, map, collect) 같은 연산은 고수준 빌딩 블록으로 이루어져 있으므로 특정 스레딩 모델에 제한되지 않고, 자유롭게 사용

-> 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없다.

 

스트림 API의 특징

1. 선언형 : 더 간결하고, 가독성이 좋다.

2. 조립할 수 있다 : 유연성

3. 병렬화 : 성능이 좋아진다.

 

스트림이란?

- 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소

 

데이터 처리 연산 : filter, map, reduce, find, match, sort

소스 : 컬렉션 or 배열 or I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비

파이프라이닝 : 스트림 연산은 스트림 연산끼리 연결해서 커다란 파이프라인 구성하여 스트림 자신을 반환

내부 반복 : 스트림은 내부 반복 (컬렉션은 외부반복)

 

List<String> threeHighCaloricDishNames =
			menu.stream()
            .filter(d -> d.getCalories() > 300)
            .map(Dish::getName)
            .limit(3)
            .collect(toList());

 

filter : 람다를 인수로 받아 스트림에서 특정 요소를 제외시킨다.(필터링)

map : 람다를 이용해서 한 요소를 다른 요소로 변환하거나 정보를 추출한다. (Dish::getName --> 요리명 추출)

limit : 정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림 크기를 축소한다.

collect : 스트림을 다른 형식으로 변환한다.

 

스트림과 컬렉션

 

차이 : 데이터를 언제 계산하느냐의 차이

 

컬렉션 : 현재 자료구조가 포함하는 모든 값을 메모리에 저장하는 자료 구조.

즉, 모든 컬렉션의 요소는 컬렉션에 담기기 전에 계산되어야 한다.

 

컬렉션에 요소를 추가하거나 삭제할 수 있다. 이러한 연산을 수행할 때마다 컬렉션의 모든 요소를 메모리에 저장해야하며,

컬렉션에 추가하려는 요소는 미리 계산되어야 한다.

 

스트림 : 요청할 때만 요소를 계산

스트림에 요소를 추가하거나 제거할 수 없다.

사용자가 요청하는 값만 스트렘에서 추출하는 것이 핵심

 

스트림은 생산자와 소비자 관계를 형성한다.

스트림은 게으르게 만들어지는 컬렉션과 같다. 즉, 사용자가 데이터를 요청할 때만 값을 계산한다.

 

반면, 컬렉션은 적극적으로 생성된다.

 

스트림은 딱 한 번만 탐색할 수 있다.

탐색된 스트림의 요소는 소비된다.

다시 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.

(그러려면 컬렉션처럼 반복 사용할 수 있는 데이터 소스여야 한다.)

만일 데이터 소스가 I/O 채널이라면, 소스를 반복 사용할 수 없으므로 새로운 스트림을 만들 수 없다.

 

스트림 :  시간적으로 흩어진 값의 집합

컬렉션 : 특정 시간에 모든 것이 존재하는 공간

 

컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복해야 한다.(for-each..따라서 외부 반복)

반면 스트림 라이브러리는 알아서 처리하고..내부반복을 사용한다.

 

스트림 연산

List<String> threeHighCaloricDishNames =
			menu.stream()
            .filter(d -> d.getCalories() > 300)
            .map(Dish::getName)
            .limit(3)
            .collect(toList());

- filter, map, limit은 서로 연결되어 파이프라인을 형성

- collect로 파이프라인을 실행한 다음에 닫는다.

 

연결할 수 있는 스트림 연산을 중간 연산, 스트림 닫는 연산을 최종 연산이라고 한다.

 

중간 연산

- filter, sorted같은 중간 연산은 다른 스트림을 반환

- 여러 중간 연산을 연결해서 질의를 만들 수 있다.

 

중간 연산의 중요한 특징

- 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하진 않는다. 즉, 게으르다.

- 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종 연산으로 한번에 처리

 

스트림의 게으른 특성으로 최적화 효과를 얻을 수 있다.

1. 300칼로리가 넘는 요리는 여러 개지만, 처음 3개만 선택! 이는 limit 연산 그리고 쇼트서킷이라 불리는 기법

2. filter, map은 서로 다른 연산이지만, 한 과정으로 병합되었다.(루프 퓨전)

 

최종 연산

 

스트림 파이프라인에서 결과를 도출

보통 List, Integerm void 등 스트림 이외의 결과가 반환.

 

스트림 이용하기

 

스트림 파이프라인 개념은 빌더 패턴과 비슷.

 

요약

  • 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.

  • 내부 반복을 지원. filter, map, sorted등의 연산으로 반복을 추상화한다.

  • 스트림에는 중간 연산과 최종 연산이 있다.
  • filter, map처럼 스트림을 반환하면서 다른 연산과 연결될 수 있는 연산은 중간 연산이다. 

  • 중간 연산을 이용해서 파이프라인을 구성할 수 있지만, 중간 연산으로는 어떠한 결과도 생성할 수 없다.

  • 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산을 최종 연산이라고 한다.

  • 스트림의 요소는 요청할 때만 계산된다.