스트림 활용
지난 장에서 외부 반복을 내부 반복으로 바꾸는 방법을 살펴봤습니다.
- 데이터를 어떻게 처리할지는 스트림 API가 관리 → 편리하게 데이터관련 작업을 할 수 있다.
- 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
- 스트림 API는 내부반복 뿐 아니라 코드를 병렬로 실행할지 결정 가능
이러한 일은 단일 스레드로 구현하는 외부 반복으로는 달성할 수 없습니다.
이번 파트는 스트림 API가 지원하는 다양한 연산을 살펴볼 것입니다.
1. 필터링
스트림의 요소를 선택하는 방법 두가지 프레디케이트 필터링 방법과 고유 요소만 필터링 하는 방법을 알아봅시다.
1.1 프레디케이트 필터링
- 스트림 인터페이스는 filter 메서드를 지원
- 프레디케이트를 인수로 받아 프레디케이트와 일치하는 모든 요소를 포함하는 스트림 반환
List<Car> koreanCar = carList.stream()
.filter(Car::isMadeKorea)
.collect(Collectors.toList());
//출력
k3
sonata
gv70
g80
avante
k5
TUCSON
이처럼 모든 국산차만 필터링 해서 국산차 리스트를 만들 수 있습니다.
1.2 고유 요소 필터링
- 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드 지원 (중복 제거)
- → 고유 여부는 스트림에서 만든 객체의 hashCode, equals 로 결정
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 4, 5, 6, 6, 7);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
//출력
2
4
6
2. 스트림 슬라이싱
이번에는 스트림의 요소를 선택하거나 스킵하는 다양한 방법을 설명합니다.
- 프레디케이트 이용하는 방법
- 스트림의 처음 몇 개의 요소를 무시하는 방법
- 특정 크기로 스트림을 줄이는 방법
2.1 프레디케이트를 이용한 슬라이싱
- 자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원합니다.
takeWhile → 처음으로 거짓이 되는 지점까지 요소를 가진다.
dropWhile → 처음으로 거짓이 되는 지점까지 요소를 버리고 남은 요소를 반환한다.
List<Car> carList = Arrays.asList(
new Car("k3", 1800, GASOLINE, true),
new Car("avante", 2300, GASOLINE, true),
new Car("sonata", 2700, HYBRID, true),
new Car("gv70", 6000, GASOLINE, true),
new Car("benzC", 6400, DIESEL, false),
new Car("g80", 8000, GASOLINE, true),
new Car("benzE", 8700, GASOLINE, false)
);
이와 같은 코드가 있습니다. 만약 6000만원 보다작은 값의 차량을 어떻게 선택할 수 있을까요? filter 를 사용하여 해결하는 방법도 있지만 위 리스트는 이미 가격별로 정렬이 되어있습니다. filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용하게 됩니다.
리스트가 이미 정렬되어있는 사실을 이용하여 6000만원이라는 기준보다 크거나 같은 요리가 나왔을 때 takeWhile or dropWhile 을 사용하여 반복작업을 중단 할 수 있습니다. 별거 아닌 것 같지만 아주많은 요소를 포함하는 큰 스트림에서는 큰 차이가 납니다.
*TAKEWHILE 활용
takeWhile → 처음으로 거짓이 되는 지점까지 요소를 가진다.
List<Car> sliceCarList = carList.stream()
.takeWhile(car -> car.getPrice() < 6000)
.collect(Collectors.toList());
//출력
k3
avante
sonata
*DROPWHILE 활용
dropWhile → 처음으로 거짓이 되는 지점까지 요소를 버리고 남은 요소를 반환한다.
List<Car> sliceCarList = carList.stream()
.dropWhile(car -> car.getPrice() < 6000)
.collect(Collectors.toList());
//출력
gv70
benzC
g80
benzE
2.2 스트림 축소
- 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드 지원
- 스트림이 정렬되어 있으면 최대 요소 n 개를 반환할 수 있다.
List<Car> sliceCarList = carList.stream()
.dropWhile(car -> car.getPrice() < 6000)
.limit(2)
.collect(Collectors.toList());
//출력
gv70
benzC
2.3 요소 건너뛰기
- 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원
List<Car> sliceCarList = carList.stream()
.dropWhile(car -> car.getPrice() < 6000)
.skip(2)
.collect(Collectors.toList());
//출력
g80
benzE
//결과가 4개인데 스킵이 4이면 빈 스트림을 반환한다.
List<Car> sliceCarList = carList.stream()
.dropWhile(car -> car.getPrice() < 6000)
.skip(4)
.collect(Collectors.toList());
//출력
//없음
3. 매핑
특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리과정에서 자주 수행되는 연산이다.
3.1 스트림의 각 요소에 함수 적용하기
- 스트림은 함수를 인수로 받는 map 메서드 지원
- 인수로 제공된 함수는 각 요소에 적용 → 결과가 새로운 요소로 매핑
List<String> sliceCarList = carList.stream()
.map(Car::getName)
.collect(Collectors.toList());
getName 은 String을 반환하므로 map 메서드의 출력 스트림은 Stream<String>의 형식을 갖습니다.
만약 차의 이름의 길이를 알고 싶다면 어떻게 해야할까요?
아래와 같이 다른 map 메서드를 연결할 수 있습니다.
List<Integer> sliceCarList = carList.stream()
.map(Car::getName)
.map(String::length)
.collect(Collectors.toList());
3.2 스트림 평면화
리스트에서 고유 문자로 이루어진 리스트를 반환하고 싶다. 어떻게 해야할까?
ex) “Hello”, “World” → H, e, l, o, W, r, d 를 포함하는 리스트
faltMap 사용
List<String> uniqueChar = word.stream()
.map(ch -> ch.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
//출력
H
e
l
o
W
r
d
- flatMap은 각 배열을 스트림이 아닌 스트림의 콘텐츠로 매핑한다.
- map(Arrays::stream) 과 달리 faltMap은 하나의 평면화된 스트림을 반환한다.
4. 검색과 매칭
- 특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용이 된다.
- 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findANy 등 다양한 메서드 제공
4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
- 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드 를 이용
- anyMatch는 boolean을 반환하므로 최종연산이다.
다음 예는 carList에 국산차가 있는지 확인하는 코드입니다.
if (carList.stream().anyMatch(Car::isMadeKorea)) {
System.out.println("국산차 있습니다.");
}
4.2 프레디 케이트가 모든 요소와 일치하는지 확인
- allMatch 메서드는 모든 요소가 주어진 프레디케이트와 일치하는지 검사
예를들어 모든 차가 6000만원이 넘는지 검사하는지 확인할 수 있습니다.
boolean isCar = carList.stream().**allMatch**(car -> car.getPrice() > 9000);
//isCar => false
프레디케이트와 일치하는 요소가 없어 false 반환
*NONEMATCH
- noneMatch는 allMatch와 반대 연산을 수행
- noneMatch는 주어진 프레디케이트와 일치하는 요소가없는지 확인
boolean isCar = carList.stream().**noneMatch**(car -> car.getPrice() > 9000);
//isCar ==> true
프레디케이트와 일치하는 요소가 없어 true 반환
*anyMatch, allMatch, noenMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, || 와 같은 연산 사용
4.3 요소 검색
- findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
- 다른 스트림 연산과 연결해서 사용 가능
- 반환타입 Optional<T>
Optional<Car> car = carList.stream()
.filter(Car::isMadeKorea)
.findAny();
*Optional이란?
- 값의 존재나 부재 여부를 표현하는 컨테이너 클래스
- 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공
findAny 에서 아무 요소도 반환하지 않을 수 있다. null 은 에러를 일으킬 수 있는 문제가 있음.
4.4 첫 번째 요소 찾기
리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야할까?
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst();
firstSquareDivisibleByThree.stream().forEach(System.out::println);
//출력 9
*findFrist 와 findAny는 그럼 언제 사용할까?
병렬 실행에서 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
5. 리듀싱
지금까지 살펴본 최종 연산은 boolean, void, Optional 객체를 반환했다.이번에는 리듀스 연산을 이용하여 스트림 요소를 조합해서 더 복잡한 질의를 표현하는 방법을 알아볼 것 이다.
ex) 자동차 중에서 가장 비싼 차는?, 자동차의 모든 가격을 합계는?
이러한 질의를 수행하려면 Integer 같은 결과가 나올 때 까지 스트림의 모든 요소를 반복적으로 처리해야 한다. 이런 질의를 리듀싱 연산 이라고 한다. 함수형 프로그래밍 언어 용어로는 이 과정이 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 불린다.
5.1 요소의합
우선 reduce를 보기전에 for-each 문을 사용하여 리스트의 숫자를 더하는 코드를 확인해보자.
int sum = 0;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (Integer x : numbers) {
sum += x;
}
numbers 의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지 reduce 과정을 반복한다. 코드에는 파라미터를 두 개 사용했다.
- sum 변수의 초기값 0
- 리스트의 모든 요소를 조합하는 연산 +
위 코드를 복붙하지 않고 모든 숫자를 곱하는 연산을 구현할 수 있다면 좋을 것 이다.
이런 상황에서 reduce를 이용하면 애플리케이션의 반복된 패턴을 추상화할 수 있다. reduce를 이용해서 다음처럼 스트림의 모든 요소를 더할 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce는 두 개의 인수를 갖는다.
- 초기값 0
- 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>, 예제에서는 람다 표현식 (a, b) → a + b 사용
다른 예로 a , b → a * b 를 사용하면 모든 요소에 곱셈을 적용할 수 있다.
초기값 없음
초기값을 받지 않도록 오버로드된 reduce 도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.
Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b);
왜 Optional 객체를 반환할까?
→ 스트림에 아무 요소도 없는 상황을 생각하면 초기값이 없어 reduce 는 합계를 반환할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.
이 리듀스를 활용하여 자동차 리스트의 전체 가격합을 구해보자.
Optional<Integer> sum = carList.stream()
.map(Car::getPrice)
.reduce((a, b) -> a + b);
이렇게 map과 reduce를 연결하는 기법을 맵 리듀스 패턴이라하며, 쉽게 병렬화하는 특징이 있다.
실제로 구글이 웹 검색에 적용하면서 유명해 졌다.
5.2 최댓값과 최솟값
최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.
Optional<Integer> sum = carList.stream()
.map(Car::getPrice)
.reduce(Integer::max);
//출력
8700
*스트림 연산 : 상태 없음과 상태 있음
map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다. 따라서 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다. 하지만 reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 예제의 내부 상태는 작은 값이다. 스트림에서 처리하는 요소의 수와 관게없이 내부 상태의 크기는 한정(bounded) 되어 있다.
반면 sorted나 distinct 같은 연산은 filter나 map처럼 스트림을 입력으로 받아 다른 스트림을 출력하는 것처럼 보일 수 있다. 하지만 sorted나 distinct는 filter나 map과는 다르다. 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다. 예를 들어 어떤 요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 연산을 수행하는 데 필요한 저장소 크기는 정해져 있지 않다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다.
이러한 연산을 내부 상태를 갖는 연산(stateful operation) 이라한다.
5.3 기본형 특화 스트림
5.1 에서 reduce를 사용하여 요소의 합을 구해봤지만 해당 코드에는 박싱 비용이 숨어있다.내부적으로 합계를 계산하기 전에 Integer 를 기본형으로 언박싱해야한다. 기본형 특화 스트림을 사용하면 박싱 비용을 피할 수 있다.
- int 요소에 특화된 IntStream
- double 요소에 특화된 DoubleStream
- long 요소에 특화된 LongStream
- 각 인터페이스는 sum, max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드 제공
- 필요할 때 다시 객체 스트림으로 복원하는 기능 제공
특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련이 있으며 스트림에 추가 기능을 제공하지 않는다.
int sum = carList.stream()
.mapToInt(Car::getPrice) //IntStream 반환
.sum();
*객체 스트림으로 복원하기
IntStream intStream = carList.stream().mapToInt(Car::getPrice);
Stream<Integer> stream = intStream.boxed();
특화 스트림을 일반 스트림으로도 변환이 가능하다.
*기본값 : OptionalInt
만약 최댓값을 찾을 때 0이라는 기본값이 있으면 잘못된 결과가 도출될 수 있다. 스트림에 요소가 없는 상황에 실제 최댓값이 0인 상황을 구별할 수 있을까?
→ 이전에는 값이 존재하는지 여부를 Optional 을 언급하였다. 이 Optional을 Integer, String 등의 참조 형식으로 파라미터화 할 수 있따. 또한 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.
OptionalInt maxPriceCar = carList.stream()
.mapToInt(Car::getPrice)
.max();
이처럼 OptionalInt 를 사용하여 IntStream의 최댓값 요소를 찾을 수 있다.
5.4 숫자 범위
- 프로그램에서 특정 범위 숫자를 이용하는 경우는 빈번하다.
- 자바 8의 IntStream 과 LongStream 에서는 range와 rangeClosed라는 두 가지 정적 메서드 제공
- 두 메서드 모두 첫 번째 인수 시작값, 두 번째 인수 종료값을 가짐
- range 메서드 → 시작값과 종료값 결과에 포함 X
- rangeClosed 메서드 → 시작값과 종료값 결과에 포함
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
//출력 50 , 100포함
IntStream evenNumbers = IntStream.range(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
//출력 49 , 100포함 안됨
6. 스트림 만들기
6.1 값으로 스트림 만들기
- 임의의 수를 인수로 받는 정적 메서드 Stream.of 를 이용하여 스트림을 만들 수 있다.
Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase)
.forEach(System.out::println);
6.2 null이 될 수 있는 객체로 스트림 만들기
- 자바 9에서 null이 될 수 있는 개체를 스트림으로 만들 수 있는 새로운 메소드 추가
- ex) System.getProperty → 제공된 키에 대응하는 속성이 없으면 null을 반환
String homeValue = System.getProperty("home");
Stream<String> homeValueStream
= Stream.ofNullable(System.getProperty("home"));
null 이 될 수 있는 객체를 포함하는 스트림값을 flatMap과 함께 사용하는 상황에서는 이 패턴을 더 유용하게 사용할 수 있다.
String homeValue = System.getProperty("home");
Stream<String> values = Stream.of("config", "home", "user")
.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
6.3 배열로 스트림 만들기
- 배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들 수 있다.
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
6.4 파일로 스트림 만들기
- 파일을 처리하는 등의 I/O 연산에 사용하는 자바 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있도록 업데이트 됨
- java.nio.file.Files의 많은 정적 메서드가 스트림을 반환
6.5 함수로 무한 스트림 만들기
- 스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate 와 Stream.generate 제공
- 두 연산을 이용해서 무한 스트림, 즉 크기가 고정되지 않은 스트림을 만들 수 있다.
- iterate 와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만듬
- 따라서 무제한으로 값을 계산할 수 있다.
- 하지만 무한한 값을 출력하지 않도록 limit(n) 함수를 함께 연결하여 사용
- 무한 스트림의 요소는 무한적 계산이 반복되므로 정렬이나 리듀스가 불가능
*iterate 메서드
Stream.iterate(0, n -> n * 2)
.limit(5)
.forEach(System.out::println);
Stream.iterate(0, n -> n * 2)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
요청이 올 때마다 값을 생성할 수 있어 무한스트림을 만든다. 이러한 스트림을 언바운드 스트림이라고 표현한다 . 이런 특징이 컬렉션과의 가장 큰 차이점이다.
*generate 메서드
Stream.generate(Math::random)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
Stream.generate(Math::random)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 iterate와 달리 generate는 생산된 각 값을연속적으로 계산하지 않는다. generate는 Supplier<T>를 인수로 받아서 새로운 값을 생성한다.
스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 불변 상태 기법을 고수해야하는데 7장에서 배울 예정이다.
마치며
- 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
- 다양한 메서드로 스트림을 필터링하거나 자를 수 있다.
- 소스가 정렬되어 있다는 사실을 알고 있으면 takeWhile 과 dropWhile 메소드 효과적 사용이 가능하다.
- map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
- findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다.
- reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다.
- 상태 없는 연산, 상태가 있는 연산이 있다.
- 기본형 특화 스트림이 있다.
- iterate 와 generate 같은 메서드로도 스트림을 만들 수 있다.
- 무한한 개수의 요소를 가진 스트림을 무한 스트림이라 한다.
'모던 자바 인 액션 스터디' 카테고리의 다른 글
모던 자바 인 액션 - 6장 스트림으로 데이터 수집 (0) | 2022.05.24 |
---|---|
모던 자바 인 액션 - 4장 스트림 소개 (0) | 2022.05.09 |
모던 자바 인 액션 - 3장 람다 표현식 (0) | 2022.05.02 |
모던 자바 인 액션 - 2장 동작 파라미터화 코드 전달하기 (0) | 2022.04.25 |