2022. 8. 22. 23:45ㆍ개발 관련 책 읽기/모던 자바 인 액션
✅ 아래 내용들에 대해서 알아보자
- 스트림이란 무엇인가?
- 컬렉션과 스트림
- 내부 반복과 외부 반복
- 중간 연산과 최종 연산
모든 실습 내용은 깃허브(아래 링크)에 있습니다. 참고 부탁드립니다 😃😃
스트림이란?
스트림이란 데이터의 일련의 흐름을 나타내는 추상적인 개념이다.
스트림의 처리는 여러 방식에서 쓰인다.
예를 들면, 파일 IO를 위한 바이트 스트림, 네트워크 소켓 통신, 파이프라인 등에서 쓰이는 개념이다.(Wiki)
자바 스트림 또한 데이터의 일련의 흐름을 추상화시켜 API를 제공한다.(아래 그림 1 참고)
간단한 예시 코드를 보면서 이해해보자.
칼로리가 300 이상인 음식들의 이름을 출력하는 기능을 개발한다고 가정하고
Stream 적용 전/후를 비교해보자.
Stream 적용 전 코드
//Stream API 적용 전
Collection<Dish> dishes = getDishList();
List<String> highCaloriesDishes=new ArrayList<>();
Iterator<Dish> iterator = dishes.iterator();
while (iterator.hasNext()){
Dish dish = iterator.next();
if(dish.getCalories() > 300){
highCaloriesDishes.add(dish.getName());
}
}
for (String highCaloriesDish : highCaloriesDishes) {
System.out.println("highCaloriesDish = " + highCaloriesDish);
}
-> Stream 적용 전 코드는 칼로리가 300 이상인 음식의 이름을 담기 위한 리스트가 필요하고
그것들을 if문을 통해서 필터링 후 List.add() 메서드를 통해 리스트에 추가 후 출력하고 있다.
Stream 적용 후 코드
//Stream API 적용 후
List<String> collect = dishes.stream()
.filter(dish -> dish.getCalories() > 300) //중간 연산
.map(Dish::getName) //중간 연산
.collect(Collectors.toList()); //최종 연산
for (String s : collect) {
System.out.println("refactoring food = " + s);
}
-> Stream 적용 후 코드는 필터링과 고칼로리 음식 이름을
filter()와 map()으로 추출 후 결괏값을 리스트화 시켜서 출력하고 있다.
Stream 적용한 코드는 아래의 그림 2처럼 Stream 연산을 연결하여 파이프라인을 형성 후 연산을 처리하고 있다.
Stream 적용 전 코드에 비해 가독성과 명확성이 유지되고 좀 더 유지 보수하기 좋은 코드가 생성된다.
번외로, filter(혹은 sorted, map, collect) 같은 연산은 고수준 빌딩 블록으로 이루어져 있어 특정 스레딩 모델에 제한되지 않고 자유롭게 어떤 상황에서든 사용할 수 있다. 결과적으로 데이터 처리 과정을 병렬화하면 스레드와 락을 걱정할 필요가 없다.
스트림 특징
파이프라이닝
대부분의 스트림 연산은 스트림을 반환하여 파이프라인을 구성할 수 있도록 한다
그 덕분에 Lazy, 쇼트서킷 같은 최적화도 얻을 수 있다.
내부 반복
반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 스트림은 내부 반복을 지원한다
순차적/병렬젹 연산
스트림은 Sequential/Parallel Stream을 통하여 순차/병렬적 연산이 가능하다
고비용연산
스트림은 고비용 연산 처리구조이다. 따라서 스트림을 속도적인 측면에서 스트림을 사용하려면
데이터 요소 개수가 충분히 크거나, CPU 연산이 비싼 상황을 고려하여 사용해야 한다.
딱 한 번만 탐색이 가능
탐색된 스트림을 재 탐색하려면 데이터 소스에서 새로운 스트림을 생성하여 탐색해야 한다.
즉, 스트림의 탐색은 한 번만 가능하다.
내부 반복 vs 외부 반복
컬렉션/스트림의 외부 반복 내부 반복에 대한 예시를 같이 보자.
1. For- each 루프를 사용한 컬렉션 외부 반복
Collection<Dish> dishes = getDishList();
//외부 반복
for (Dish dish : dishes) {
System.out.println("dish.getName() = " + dish.getName());
}
2. Iterator를 사용한 컬렉션 외부 반복
Collection<Dish> dishes = getDishList();
Iterator<Dish> iterator = dishes.iterator();
while(iterator.hasNext()){ <- 명시적 반복
System.out.println("iterator = " + iterator.next().getName());
}
->컬렉션 인터페이스는 사용자가 직업 요소를 반복해서 사용해야 한다
3. 스트림 내부 반복
Collection<Dish> dishList = getDishList();
List<String> names = dishList.stream()
.map(Dish::getName) <- 파이프라인 실행
.collect(Collectors.toList()); <- 반복자가 필요없다
-> 결과적으로 내부 반복이 외부 반복보다 좋은 점은 연산을
병렬로 처리하거나 더 최적화된 다양한 순서로 처리가 가능하다.
반면 for-each를 사용한 외부 반복에서는 병렬성을 스스로 관리해야 한다.
정리
- 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.
- 스트림은 내부 반복을 지원한다. 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화한다.
- 스트림은 중간 연산과 최종 연산이 있다
- 중간 연산은 filter와 map처럼 스트림을 반환하면서 다른 연산과 연결되는 연산이다. 중간 연산으로는 파이프라인을 구성할 수 있지만 어떤 결과도 생성할 수 없다.
- foreach, count, collect와 같이 결과를 반환하는 연산을 최종 연산이라고 한다.
- 스트림의 요소는 요청할 때 Lazy 하게 계산된다.
참고자료
'개발 관련 책 읽기 > 모던 자바 인 액션' 카테고리의 다른 글
Chapter 6 - 스트림으로 데이터 수집 (0) | 2022.09.25 |
---|---|
Chapter 5 - 스트림 활용(1편) (0) | 2022.09.04 |
Chapter 3 - 람다 표현식 (0) | 2022.08.22 |
Chapter 2 - 동작 파라미터화 코드 전달하기 (0) | 2022.08.22 |
Chapter 1 - 자바 8, 9, 10, 11 : 무슨 일이 일어나고 있는가 (0) | 2022.08.22 |