모던 자바 인 액션 정리본
스트림
스트림은 자바 8 API에 새로 추가된 기능
스트림 특징
스트림을 사용하면 선언형으로 컬렉션 데이터를 처리할 수 있다. ( 데이터를 직접 처리하는 구현 코드 대신 질의로 표현 )
또한 멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.
filter, sorted, map , collect 같은 연산은 고수준 빌딩 블록으로 이루어져 특정 스레드 모델에 제한되지 않고 자유롭게 사용할 수 있다.
따라서 데이터 처리 과정을 병렬화하면 스레드와 락을 걱정할 필요가 없다.
- 선언형 : 더 간결하고 가독성이 좋아진다.
- 조립할 수 있음 : 유연성이 좋아진다.
- 병렬화 : 성능이 좋아진다.
스트림 정의
스트림이란 데이터 처리 연산을 지원하도록 소스에서 추출된 연속된 요소로 정의할 수 있다.
- 연속된 요소 : 특정 요소 형식으로 이루어진 연속된 값집합의 인터페이스를 제공 filter, sorted, map 등의 표현 계산식이 주를 이룬다.
- 소스 : 컬렉션, 배열 , I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비한다.
- 데이터 처리 연산 : 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 데이터베이스와 비슷한 연산 지원, 순차적, 병렬로 실행 가능
- 파이프라이닝 : 스트림 연산끼리 연결해서 커다란 파이프라인을 구성할 수 있도록 스트림 자신을 반환 가능
- 내부 반복 : 반복자를 이용해서 명시적으로 반복하는 컬렉션과 달리 내부 반복을 지원
지금까지 나온 단어들을 코드로 살펴보자
List<String> threeHighCaloricDishNames =
menu.stream()//메뉴 ( 요리 리스트 ) 에서 스트림을 얻는다.
.filter(dish -> dish.getCalories() > 300)//파이프라인 연산 만들기, 첫 번째로 고칼로리 요리를 필터링
.map((Dish::getName))//요리명 추출
.limit(3)//선착순 3 개만 선택
.collect(toList());//결과를 다른리스트로 저장
데이터 소스 : 요리 리스트 (메뉴)
데이터 처리 연산 : filter, map, limit, collect로 이어지는 일련의 데이터 처리 연산 제공
파이프라인 : collect를 제외한 모든 연산은 서로 파이프라인 형성
- filter : 람다를 인수로 받아 스트림에서 특정 요소 제외
- map : 람다를 이용해서 한 요소를 다른 요소로 변환하거나 정보 추출
- limit : 정해진 개수 이상의 요소가 스트림에 저장되지 못하게 스트림 크기 축소
- collect : 스트림을 다른 형식으로 변환
스트림과 컬렉션의 차이
공통점 : 스트림과 컬렉션은 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공한다.
차이점 : 컬렉션은 모든 값을 메모리에 저장하는 자료구조, 즉 컬렉션의 모든 요소는 컬렉션에 추가하기 전에 계산되어야 한다. (추가, 삭제 가능), 스트림은 이론적으로 요청할 때만 요소를 계산하는 고정된 자료구조, 즉 스트림에 요소를 추가하거나 제거할 수 없다. 생산자, 소비자 관계, 컬렉션은 생산자 중심( 팔기도 전에 창고를 가득 채움)이다.
스트림은 반복자와 마찬가지로 한번만 탐색할 수 있다. 탐색된 스트림의 요소는 소비된다.
외부 반복과 내부 반복
컬렉션 인터페이스를 사용하려면 사용자가 직접 요소를 반복 ( for - each )
스트림 라이브러리는 내부반복을 사용한다.
외부 반복
List<String> names = new ArrayList<>();
for(Dish dish:menu){
names.add(dish.getName());
}
내부 반복
List<String> names = menu.stream()
.map(Dish::getName)
.collect(toList());
내부 반복이 좋은 이유 : 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있다.
스트림 연산
중간 연산 : 연결할 수 있는 스트림 연산 ( filter, map, limit ) 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다.(순서대로 실행)
최종 연산 : 스트림을 닫는 연산 (collect) , 스트림 파이프라인에서 결과를 도출한 후 최종 연산에 의해 List, Integer 등 스트림 이외의 결과가 반환된다.
필터링
1. 프레디케이트로 필터링
Boolean을 반환하는 함수를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
2. 고유 요소 필터링
고유 요소로 이루어진 disinct 메서드도 지원한다.
스트림 슬라이싱
스트림 요소를 선택하거나 스킵하는 방법
1. 프레디케이트를 이용한 슬라이싱
takewhile
List<Dish> filteredMenu = menu.stream()
.filter(dish -> dish.getCalories() < 320)
.collect(toList());
위와같은 스트림을 활용한 코드는 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용하게 된다.
하지만 리스트가 정렬되어 있다면 모든 스트림을 반복하지 않고 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복 작업을 중단하게 하려 할 때 takewhile을 사용하면 된다.
List<Dish> filteredMenu = menu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
320 칼로리보다 큰 요소를 탐색하려면 dropwhile을 이용할 수 있다.
List<Dish> filteredMenu = menu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
또한 Limit 메서드를 이용하여 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환할 수 있다.
List<Dish> filteredMenu = menu.stream()
.filter(dish -> dish.getCalories() < 320)
.limit(3)
.collect(toList());
skip 메서드를 이요하여 처음 n개 요소를 제외한 스트림을 반환할 수 있다.
List<Dish> filteredMenu = menu.stream()
.filter(dish -> dish.getCalories() < 320)
.skip(3)
.collect(toList());
매핑
특정 객체에서 특정 데이터를 선택하는 작업
스트림에서 map, flatMap메서드는 특정 데이터를 선택하는 기능을 제공한다.
- map
함수를 인수로 받아 각 요소에 적용되어 함수를 적용한 결과가 새로운 요소로 매핑된다. ( 기존의 값을 수정하는게 아니라 새로 만든다는 개념에 가까워 mapping이라는 단어 사용 )
List<String> filteredMenu = menu.stream()
.map(Dish::getName)
.collect(toList());
getname은 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String> 형식을 갖는다.
스트림 평면화
리스트에서 고유 문자로 이루어진 리스트를 반환하려면 flatmap을 이용하여 리스트를 반환할 수 있다.
map을 사용하게 되면
List<String> words = List.of("Hello","World");
words.stream().map(word -> word.split("")).distinct().collect(toList());
위의 코드는 ["Hello","World"]를 반환하게 된다. map으로 전달한 람다는 각 단어의 문자열 배열을 반환하여 map 메서드가 반환한 스트림은 Stream(String [])이다. 따라서 Stream(String) 형태가 나오지 않는다.
flatMap이라는 메서드를 이용해서 Stream(String) 형태로 나올 수 있다
문자열을 받아 스트림을 만드는 Arrays.stream 메서드를 적용하면 각 배열을 별도의 스트림으로 생성할 수 있다.
List<String> words = List.of("Hello","World");
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());
검색과 매칭
프레디케이트가 적어도 한 요소와 일치하는지 확인 : anyMatch(return boolean)
프레디케이트가 모든 요소와 일치하는 지 검사 : allMatch (return boolean)
현재 스트림에서 임의요소를 반환 : findAny
첫 번째 요소 찾기 : findFirst
리듀싱
리듀스 연산을 사용하여 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 할 수 있다.
결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리해야 한다.
reduce는 두 개의 인수를 갖는다.
- 초깃값
- 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator (a, b) -> a+b
int sum = numbers.stream().reduce(0,(a,b) -> a+b);
초깃값이 없다면 Optional 객체를 반환한다.
최댓값과 최솟값을 찾을 때도 reduce를 사용할 수 있다.
Optional<Integer> min = numbers.stream().reduce(Integer::min);
숫자형 스트림
reduce를 사용하여 스트림 요소의 합을 구할 수 있었다.
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
하지만 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다는 비용이 들어있다.
직접 sum 메서드를 호출하여 비용을 줄일 수 있다.
int calories = menu.stream()
.map(Dish::getCalories)
.sum();
자바에선 기본형 특화 스트림을 제공하여 박싱 비용을 피할 수 있도록 해준다.
IntStream, DoubleStream, LongStream을 제공한다.
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
지금까지 스트림에 대한 기본적인 내용을 알아보았다.
다음 포스트는 스트림으로 데이터 수집을 어떻게 할 것인가에 대해 알아보겠다.
'자바' 카테고리의 다른 글
JNI 사용하기 (0) | 2024.09.19 |
---|---|
동시성 제어하기 (0) | 2023.10.05 |
JVM이란 (0) | 2023.01.16 |
람다 표현식 (1) | 2023.01.11 |
동적 파라미터화 (0) | 2023.01.10 |