[자바 인 액션] 04 - 스트림 소개
Chapter 04. 스트림 소개
1. 스트림이란 무엇인가?
스트림을 이용하면 선언형
1으로컬렉션 데이터를 처리할 수 있다.
-
기존 코드(자바 7)
- lowCaloricDishes라는 가비지 변수(컨테이너 역할만 하는 중간 변수)를 사용
List<Dish> lowCaloricDishes = new ArrayList<>(); for(Dish d: menu) { `if(d.getCalories() < 400) { lowCaloricDishes.add(d); } } Collections.sort(lowCaloricDishes, new Comparator<Dish>() { public int compare(Dish d1, Dish d2) { return Integer.compare(d1.getCalories(), d2.getCalories()); } }); List<String> lowCaloricDishesName = new ArrayList<>(); for(Dish d: lowCaloricDishes) { lowCaloricDishesName.add(d.getName()); }
-
자바 8
- 세부 구현은 라이브러리 내에서 모두 처리하고 어떻게 처리할지 질의만 정의한다.
List<String> lowCaloricDishesName = menu.stream() .filter(d -> d.getCalories() < 400) .sorted(comparing(Dishes::getCalories)) .map(Dish::getName) .collect(toList());
stream()
을parallelStream()
으로 바꾸면 이 코드를멀티코어
아키텍처에서병렬
로 실행할 수 있다.
List<String> lowCaloricDishesName = menu.parallelStream() .filter(d -> d.getCalories() < 400) .sorted(comparing(Dishes::getCalories)) .map(Dish::getName) .collect(toList());
다음 질문의 답은 7장에서 설명
parallelStream을 호출 했을 때 정확히 어떤 일이 일어날까?
- 얼마나 많은 스레드가 사용되는 걸까?
- 얼마나 성능이 좋을까?
스트림 이점
- 선언형으로 코드를 구현할 수 있다.
- 어떻게 동작해야하는지 지정할 필요 없이 동작의 수행만 지정
- filter, sorted, map, collect 같은 여러 빌딩 블록 연산을 연결해서 복잡한 데이터 처리 파이프라인을 만들 수 있다.
- 가독성과 명확성이 유지된다.
filter(sorted, map, collect) 같은 연산은 고수준 빌딩 블록 으로 이루어져 있으므로 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있다.
(내부적으로 단일 스레드 모델에 사용할 수 있지만 멀티코어 아키텍처를 최대한 투명하게활용할 수 있게 구분되어 있다.) 결과적으로 사용자가 데이터 처리 과정을 병렬화하면서 스레드와 락을 걱정할 필요가 없다.
스트림 특징
- 선언형 : 더 간결하고 가독성이 좋아진다.
- 조립할 수 있음 : 유연성이 좋아진다.
- 병렬화 : 성능이 좋아진다.
4.2 스트림 시작하기
스트림이란 정확히 뭘까?
스트림이란 ‘데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소‘로 정의할 수 있다.
-
연속된 요소
컬렉션
과 마찬가지로스트림
은 특정 요소 형식으로 이루어진 연속된 값 집합의인터페이스
를 제공한다.-
컬렉션의 주제 -
데이터
컬렉션은 자료구조로 시간과 공간의 복잡성과 관련된 요소 저장 및 접근 연산
-
스트림의 주제 -
계산
표현 계산식이 주를 이룬다.
-
-
소스 스트림
컬렉션
,배열
,I/O 자원
등의 데이터 제공 소스로부터 데이터를 소비(consume
)한다.리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지한다.
-
데이터 처리 연산
스트림 연산은
순차적
으로 또는병렬
로 실행할 수 있다.
스트림 특징
-
파이프라이닝
대부분의 스트립 연산은 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환한다.
⇒ 그 덕분에, 게으름(laziness), 쇼트서킷(short-circuiting) 최적화를 얻을 수 있다.
연산 파이프라인은
데이터 소스
에 적용하는 데이터베이스질의
와 비슷하다. -
내부 반복
반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은
내부 반복
을 지원한다.
연산 | 작업 | 반환타입 |
---|---|---|
filter | 람다를 인수로 받아 특정 요소 제외 | 스트림 |
map | 람다를 이용해 한 요소를 다른 요소로 변환/ 정보 추출 | 스트림 |
limit | 스트림 크기를 축소 truncate | 스트림 |
collect | 스트림을 다른 형식으로 변환 | 다른 형식 |
4.3 스트림과 컬렉션
- 둘 다 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
- “연속된” - “순차적으로 값에 접근한다.”는 의미
데이터를 언제 계산하느냐가 컬렉션과 스트림의 가장 큰 차이라고 할 수 있다.
컬렉션
- 현재 자료구조가 포함하는
모든 값
을메모리
에 저장하는 자료구조
컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다.
(컬렉션에 요소를 추가/삭제할 수 있다. 이런 연산을 수행할 때마다 컬렉션의 모든 요소를 메모리에 저장해야 하며 컬렉션에 추가하려는 요소는 미리 계산되어야 한다.)
-
적극적으로 생성된다.
즉, 모든 값을 계산할까지 기다린다.
스트림
- 이론적으로
요청
할 때만 요소를 계산하는고정된 자료구조
(스트림에 요소를 추가/제거할 수 없다.)
-
“사용자가 요청하는 값만 스트림에서 추출한다.”는 것이 핵심
-
스트림은 게으르게 만들어지는 컬렉션과 같다.
즉, 사용자가 데이터를 요청할 때만 값을 계산한다.
4.3.1 딱 한 번만 탐색할 수 있다.
탐색된 스트림의 요소는 소비된다.
재 탐색하려면 초기 데이터 소스에서 새로운 스트림을 만들어야 한다.(그러려면 컬렉션처럼 반복 사용할 수 있는 데이터 소스여야 한다. 데이터 소스가 I/O 채널이라면 소스를 반복사용할 수 없으므로 새로운 스트림을 만들 수 없다.)
4.3.2 외부 반복과 내부 반복
컬렉션 인터페이스
외부 반복
- 사용자가직접
요소를 반복해야 한다. (ex. for-each)
스트림
라이브러리
내부 반복
을 사용 - 반복을 알아서 처리하고 결과스트림값을 어딘가에 저장해준다.- 함수에 어떤 작업을 수행 할 지만 지정하면 모든 것이 알아서 처리된다.
// 외부 반복
List<String> names = new ArrayList<>();
for(Dish d:menu) { // 메뉴 리스트를 명시적으로 순차 반복
names.add(d.getName()); //이름을 추출해서 리스트에 추가
}
// 내부 반복
List<String> names = menu.stream()
.map(Dish::getName) //map 메서드를 getName 메서드로 파라미터화해서 요리명을 추출
.collect(toList()); //파이프라인을 실행한다. 반복자 필요없음
외부 반복보다 내부 반복이 더 좋은 이유
- 작업을 투명하게 병렬로 처리
- 더 최적화된 다양한 순서로 처리
스트림 라이브러리의 내부 반복
은 데이터 표현
과 하드웨어
를 활용한 병렬성 구현을 자동으로 선택한다.
반면 for-each
를 이용하는 외부 반복
에서는 병렬성
을 스스로 관리해야 한다.
병렬성
을 스스로 관리한다” ? → 병렬성을포기
하든지 아니면synchronized
로 시작하는 힘들고 긴 전쟁을 시작함을 의미한다.
스트림은 내부 반복을 사용하므로 반복 과정을 우리가 신경 쓰지 않아도 된다. 그러려면 반복을 숨겨주는 연산 리스트
가 미리 정의되어 있어야 한다. 이 연산은 람다 표현식
을 인수로 받기 때문에 동작 파라미터화
를 활용할 수 있다.
4.4 스트림 연산
스트림 인터페이스의 연산을 크게 두 가지(중간 연산, 최종 연산)로 구분할 수 있다.
- 중간 연산 - 서로 연결되어 파이프라인을 형성
- 최종 연산 - 파이프라인을 실행한 다음에 닫는다.
List<String> names = menu.stream()
.filter(d -> d.getCalories() > 300) // 중간 연산
.map(Dish::getName) // 중간 연산
.limit(3) // 중간 연산
.collect(toList()); // 최종 연산
중간 연산
중간 연산의 중요한 특징은 단말 연산
을 스트림 파이프라인
에 실행하기 전까지는 아무 연산도 수행하지 않는다. 즉 게으르다(lazy)
는 것이다.
스트림 파이프라인의 처리 과정을 확인해보기 위해 람다가 현재 처리 중인 데이터를 출력해서 확인해보자 (학습용으로만 )
List<String> names = menu.stream()
.filter(d -> {
System.out.println("filtering" + d.getName());
return d.getCalories() > 300;
})
.map(d -> {
System.out.println("mapping" : g.getName());
return d.getName();
})
.limit(3)
.collect(toList());
// 출력 결과
// filtering pork
// mapping pork
// filtering beef
// mapping beef
// filtering chicken
// mapping chicken
출력 결과를 보자. 스트림의 게으른 특성 덕분에 몇 가지 최적화 효과를 얻을 수 있다.
- limit 연산 그리고 쇼트서킷 : 300 칼로리가 넘는 요리는 여러 개지만 오직 처음 3개만 선택되었다.
- 루프 퓨전 : filter와 map은 서로 다른 연산이지만 한 과정으로 병합되었다.
중간 연산 종류
연산 | 형식 | 연산의 인수 | 함수 디스크립터 |
---|---|---|---|
filter | 중간 연산 | Predicate |
T -> boolean |
map | 중간 연산 | Function<T,R> | T -> R |
limit | 중간 연산 | ||
sorted | 중간 연산 | Conparator |
(T, T) -> int |
distinct | 중간 연산 |
최종 연산
최종 연산은 스트림 파이프라인에서 결과
를 도출한다. 보통 최종 연산에 의해 List
, Integer
, void
등 스트림 이외의 결과가 반환된다.
최종 연산 종류
연산 | 형식 | 목적 | 반환 형식 |
---|---|---|---|
forEach | 최종 연산 | 스트림의 각 요소를 소비하면서 람다를 적용한다. void를 반환한다. | void |
count | 최종 연산 | 스트림의 요소 개수를 반복한다. long을 반환한다. | long(generic) |
collect | 최종 연산 | 스트림을 리듀스해서 리스트, 맵, 정수 형식의 컬렉션을 만든다. |
정리
- 스트림은 소스에서 추출된
연속 요소
로,데이터 처리 연산
을 지원한다. - 스트림은
내부 반복
을 지원한다. 내부 반복은filter
,map
,sorted
등의 연산으로 반복을 추상화한다. - 스트림에는
중간 연산
과최종 연산
이 있다. filter
와map
처럼 스트림을 반환 하면서 다른 연산과 연결될 수 있는 연산을중간 연산
이라고 한다. 중간 연산을 이용해서파이프라인
을 구성할 수 있지만중간 연산
으로는 어떤 결과도 생성할 수 없다.forEach
나count
처럼 스트림 파이프라인을 처리해서 스트림이 아닌 결과를 반환하는 연산을최종 연산
이라고 한다.- 스트림의 요소는
요청
할 때만 계산된다.
-
선언형 - 데이터를 처리하는 임시 구현 코드 대신 질의로 표현 ↩