Originally posted by **MyeoungDev** June 5, 2023
# 📕 Modern Java In Action Chapter_5
# 5.0 스트림 활용
스트림 API는 내부 반복을 통해 다양한 최적화가 이루어질 수 있다.
또한, 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다.
# 5.1 필터링
스트림의 요소를 선택하는 방법은 두가지가 존재한다.
**Predicate를 이용한 필터링**, **고유 요소 필터링** 두가지 존재한다.
## Predicate 필터링
```java
/* 모든 요리 중 채식요리 필터링 예제 */
List vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/72f0e1e7-8266-4e93-81de-d29931949b8d)
## 고유 요소 필터링
스트림은 고유 요소로 이루어진 스트림을 반환하는 `distinct` 메서드도 지원한다.
여기서, 고유 여부는 스트림에서 만든 객치의 `hashCode`, `equals` 로 결정된다.
```java
/* distinct 를 이용한 필터링 */
List numbers = Arrays.asList(1, 2, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/dbcd79af-c9fa-4052-a577-479e1664eb48)
# 5.2 스트림 슬라이싱
스트림 슬라이싱은 Java 9의 새로운 기능으로 스트림의 요소를 선택하거나 스킵, 무시 등에 대한 효율적으로 작업을 수행 할 수 있는 기능이다.
## Predicate를 이용한 슬라이싱
Java 9은 스트림의 요소를 효과적으로 선택할 수 있도록 `takeWhile` , `dropWhile` 두 가지 새로운 메서드를 지원한다.
### takeWhile 활용
해당 메서드는 **정렬되어있는 스트림을 해당 기준으로 슬라이싱 하는것에 대해 매우 효과적**이다.
```java
/* 칼로리 순으로 정렬되어있는 리스트 */
List specialMenu = Arrays.asList(
new Dish("seasonal fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER)
);
/* 기존 filter를 이용한 연산*/
List filteredMenu
= specialMenu.stream()
.filter(dish -> dish.getCalories() < 320)
.collect(toList());
/* takeWhile을 이용한 연산 */
List slicedMenu1
= specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
```
위의 코드에서 `filter` 연산과, `takeWhile` 의 가장 큰 차이점은 연산을 (Predicate) 어디까지 적용하나 이다.
`filter` 연산의 경우 **전체 스트림을 반복**하면서 **각 요소에 Predicate를 적용하여 연산**하게 된다.
`takeWhile` 는 리스트가 **이미 정렬**되어 있다는 장점을 이용하여 **해당 조건(Condition)까지만 연산을 진행**한다.
만약, 아주 많은 요소를 갖고있는 큰 스트림의 경우 이에 대한 차이는 커질 것이다.
### dropWhile
`dropWhile` 의 경우에는 `takeWhile` 과 반대로 **나머지 요소를 선택하고 싶을 경우**에 사용하면 된다.
Predicate의 조건이 false가 되면 그 지점에서 작업을 중단하고 남은 요소를 반환한다.
```java
List slicedMenu2
= specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
```
## 스트림 축소
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 `limit(n)` 메서드를 지원한다.
```java
List dishes = specialMenu
.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
```
이때, `limit(n)` 의 경우 즉시 선택된 경우에 n만큼 선택하여 반환한다.
그 말은 정렬되어 있는 요소를 실행할 경우 정렬되어 있는 요소가 반환 될 것이고,
`Set` 의 경우 정렬되어 있지 않은 요소가 반환 될 것이다.
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/cd4f3dd3-fe1a-4d2a-8fe7-e04e52dafdd1)
## 스트림 요소 건너뛰기
스트림은 처음 n개의 요소를 제외한 스트림을 반환하는 `skip(n)` 메서드를 지원한다.
`skip(n)` 메서드는 `limit(n)` 메서드와 상호 보완적인 연산을 수행한다.
```java
List dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/fb9672c6-90a2-4d65-b6e0-838b70274929)
# 5.3 매핑
특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다.
Stream API의 `map` 과 `flatMap` 메서드는 특정 데이터를 선택하는 기능을 제공한다.
```java
/* 각 요리의 이름 리스트를 반환 */
List dishNames = menu.stream()
.map(Dish::getName)
// map(Dish::getName)은 Stream을 반환한다.
.collect(toList());
/* 각 요리 이름의 길이 리스트 반환 */
List dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
```
이를 이용해 각 단어를 `split` 하여 고유 문자로만 이루어진 하나의 리스트를 반환한다고 가정해보자.
예를 들어 `["Hello", "World"]` 리스트가 있다면 `["H," "e," "l," "o," "W," "r," "d"]` 의 리스트를 반환해보자.
```java
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
```
위와 같이 코드를 작성할 수 있겠다.
하지만, 위의 `map()` 메서드가 반환하는 형식은 `Stream` 이 되게 된다.
즉, 결과는 `List` 이 되게 되고, `[[”H”, “e”, “l”, “l”, “o”], [”W”, “o”, “r”, “l”, “d”]]` 가 되게 된다.
아래의 그림의 위의 코드에 변환 과정을 나타낸 것으로 시각적으로 더 쉽게 이해가 가능하다.
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/5dd28a30-937f-47f7-a924-d9a310b533a3)
위의 문제를 해결하기 위해 `flatMap` 메서드를 사용하면 된다.
`flatMap` 은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다.
즉, `map` 과 달리 하나의 평면화된 스트림을 반환한다.
코드와 그림으로 이해해 보자.
```java
List uniqueCharacters =
words.stream()
.map(word -> word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 변환
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/30c4f425-fcc2-4475-af75-36a7f1ddbec0)
# 5.4 검색과 매칭
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
Stream API는 `allMatch` , `anyMatch`, `noneMatch` , `findFirst`, `findAny` 등 다양한 유틸리티 메서드를 제공한다.
```java
/* anyMatch 적어도 한 요소가 주어진 Prdicate와 일치하는지 확인 */
if(menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
/* allMatch 모든 요소가 주어진 Predicate와 일치하는지 확인 */
boolean isHealthy = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
/* noneMatch 주어진 Predicate와 일치하는 요소가 없는지 확인 */
boolean isHealthy = menu.stream()
.noneMatch(d -> d.getCalories() >= 1000);
/* findAny 메서드는 현재 스트림의 임의의 요소를 반환. return Optional */
Optional dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
/* findFirst 스트림 요소 중 첫번째 요소를 찾아서 반환. return Optional */
List someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional firstSquareDivisibleByThree =
someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst(); // 9
```
`allMatch`, `noneMatch`, `findFirst`, `findAny` 등의 연산은 **쇼트서킷** 연산을 한다.
쇼트서킷이란 표현식에서 하나라도 거짓의 결과가 나오면 나머지 표현식의 결과와 상관없이 거짓이 나오는 것 처럼
**모든 스트림의 요소를 처리하지 않고도 결과를 반환하는 것이다.**
원하는 요소를 찾으면 즉시 결과를 반환한다.
마찬가지로 스트림의 모든 요소를 처리할 필요 없이 주어진 크기의 스트림을 생성하는 `limit` 도 쇼트서킷 연산이다.
또한, 위의 코드에서 `findAny` 와 `findFirst` 의 경우 return 타입이 `Optional` 이다.
## Optional이란?
`Optional` 클래스(java.util.Optional) 는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다.
Java에서 null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 `Optional` 를 만들었다.
`Optional` 은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리할지 강제하는 기능을 제공한다.
- `isPresent()`
- Optional이 값을 포함하면 true, 값을 포함하지 않으면 false
- `ifPresent(Consumer block)`
- 값이 있으면 주어진 블록을 실행
- `T get()`
- 값이 존재하면 값을 반환. 값이 없으면 `NoSuchElementException`
- `T orElse (T other)`
- 값이 있으면 값을 반환, 값이 없으면 기본값을 반환
> `findAny`, `findFirst` 둘은 언제 사용하나?
두 메서드 모두 하나의 값을 찾아서 반환하는 메서드이다. 그런데 왜 두개 다르게 존재할까?
>
>
> 바로 병렬성 때문이다.
> 병렬 실행에서는 첫 번째 요소를 찾기 어렵다.
> 따라서, 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
>
# 5.5 리듀싱
스트림 요소를 조합해서 더 복잡한 질의를 표현하는 법을 알 수 있다.
모든 스트림의 요소를 처리해서 값으로 도출하는 것을 **리듀싱 연산** 이라고 한다.
함수형 프로그래밍 언어 용어로는 마치 종이를 작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 **폴드** 라고 부른다.
```java
/* for-each 루프를 이용하여 리스트의 숫자 요소를 더하는 코드 */
int sum = 0;
for (int x : numbers) {
sum += x;
}
/* reduce를 이용한 애플리케이션의 반복된 패턴을 추상화 */
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
/* 메서드 참조를 이용해 좀 더 간결한 코드 */
int sum = numbers.stream().reduce(0, Integer::sum);
/* 초기값을 받지 않도록 오버로드된 reduce */
Optional sum = numbers.stream().reduce((a, b) -> (a + b));
// 스트림에 아무 요소도 없는 상황에서 초기값이 없으므로 reduce는 합계를 반환할 수 없다.
// 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/533e7776-cf5c-47c4-b8e6-851dc7286c45)
최대값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.
```java
/* reduce를 이용한 최대값 Integer.max(int a, int b) 메서드 참조 */
Optional max = numbers.stream().reduce(Integer::max);
/* reduce를 이용한 최솟값 Integer.min(int a, int b) 메서드 참조 */
Optional min = numbers.stream().reduce(Integer::min);
```
![image](https://github.com/MyeoungDev/MyeoungDev/assets/73057935/3e9b7c12-05b3-4e9d-86d6-f114457f7d34)
> **reduce메서드의 장점과 병렬화**
도대체 기존의 단계적 반복으로 합계를 구하는 것과 `reduce`를 이용해서 합계를 구하는 것이 어떤 차이가 있을까?
`reduce` 를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 `reduce` 를 실행 할 수 있게 된다.
반복적인 합계에서는 `sum` 이라는 변수를 공유해 하므로 쉽게 병렬화 하기 어렵다.
강제적으로 동기화시키더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에 상쇄되어 버린다.
추후 7장에서 이에 대해 자세한 설명을 기술한다.
바로, `stream()` 을 `parallelStream()` 으로 바꿔서 처리하는 방법인데, 이렇게 처리하게 된다면, 병렬성을 얻을 수 있는 대신 몇가지 제약 사항이 따르게 된다.
이는 추후에 더 자세히 설명한다.
>
> 스트림 연산 : 상태 없음 (stateless operation)과 상태 있음 (stateful operation)
지금까지 다양한 스트림 연산을 살펴 보았다.
스트림은 다양한 연산을 병렬로 실행할 수 있게 해주었다.
각각 다양한 연산을 실행해주는 만큼, 각각의 연산은 내부적인 상태를 고려해야 한다.
map, filter 등은 입력 스트림에서 각 요소를 받아 결과를 출력 스트림으로 보낸다.,
따라서 이들은 **내부 상태를 갖지 않는 연산(statelss operation)이다.**
반면 sorted나 distinct 같은 연산은 이전 연산에 대한 정보를 알고 있어야 한다.
예를 들어 어떤 요소를 출력 스트림으로 추가하려면 **모든 요소가 버퍼에 추가되어 있어야 한다.
이러한 연산을 내부 상태를 갖는 연산(stateful operation) 이라 한다.**
>
# 5.7 기본형 특화 스트림
Java 8에서는 세 가지 기본형 특화 스트림을 제공한다.
Stream API는 박싱 비용을 피할 수 있도록 int 요소에 특화된 `IntStream`, double 요소에 특화된 `DoubleStream`, long 요소에 특화된 `LongStream` 을 제공한다.
각각의 인터페이스는 sum, max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.
또한, 필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.
특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하지는 않는다.
스트림을 특화 스트림으로 변환할 때는 `mapToint`, `mapToDouble`, `mapToLong` 세 가지 메서드를 가장 많이 사용한다.
```java
/* IntStream 반환을 통한 sum 메서드 호출 */
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum(); // 기본값 0
/* 기본형 특화 스트림을 다시 객체 스트림으로 변환 */
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream stream = intStream.boxed();
```
### 기본값 : OptionalInt
IntStream에서 최댓값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.
이때, 스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 구별하기 힘들다.
따라서, `Optional` 을 `Integer`, `String` 등의 참조 형식으로 파라미터화할 수 있다.
또한, `OptionalInt`, `OptionalDouble`, `OptionalLong` 세 가지 기본현 특화 스트림 버전도 제공한다.
```java
/* OptionalInt 예제 */
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); // 값이 없을 경우 기본값을 명시적으로 설정
```
### 숫자 범위
프로그램에서는 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.
`IntStream` 과 `LongStream` 에서는 `range` 와 `rangeClosed` 라는 두 가지 정적 메서드를 제공한다.
두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.
`range` 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면 `rangeClosed` 는 시작값과 종료값이 결과에 포함된다.
```java
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
```
# 5.8 스트림 만들기
스트림이 데이터 처리 질의를 표현하는 강력한 도구임을 확인했다.
그렇다면 이 스트림을 다양한 방식으로 만들어 다루르는 것도 중요할 것이다.
```java
/* 값으로 스트림 만들기 */
Stream stream = Stream.of("Modern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);
/* empty를 이용하여 비어있는 스트림 만들기 */
Stream emptyStream = Stream.empty();
/* null이 될 수 있는 객체로 스트림 만들기 */
String homeValue = System.getProperty("home");
Stream homeValueStream = homeValue == null ? Stream.empty() : Stream.of(value);
/* Stream.ofNullable 을 이용한 스트림 만들기 */
Stream homeValueStream = Stream.ofNullable(System.getProperty("home"));
/* null이 될 수 있는 객체를 포함하는 스트림값을 flatMap 과 함께 사용하기 */
Stream values = Stream.of("config", "home", "user")
.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
/* 배열로 스트림 만들기 */
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
/* 파일로 스트림 만들기 */
long uniqueWords = 0;
// 스트림은 AutoCloseable 이므로 try-with-resources 사용
try(Stream lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
}
catch(IOException e){
// 예외처리
}
```
## 함수로 무한 스트림 만들기
Stream API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 `Stream.iterate` 와 `Stream.generate` 를 제공한다.
여기서 **무한스트림이란 크기가 고정되지 않은 정말 무한한 스트림을 만든다는 것이다.**
이러한 스트림을 **언바운드 스트림(unbounded stream)** 이라고 표현한다.
따라서, 정말 무한한 값을 사용하는 것이 아닌이상 `limit` 함수를 함께 사용한다.
또한, Java 9에서 `iterate` 메서드의 경우 `Predicate` 를 지원함으로 이를 사용하여 언제까지 작업을 수행할 것의 기준을 잡을 수도 있다.
```java
/* 무한스트림 limit 사용 */
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
/* 무한스트림 Prdicate 사용 */
IntStream.iterate(0, n -> n < 100, n -> n + 4)
.forEach(System.out::println);
```
`iterate` 와 비슷하게 `generate` 도 요구한 값을 계산하는 무한 스트림을 만들 수 있다.
하지만, `iterate` 와 달리 `generate` 는 생산된 각 값을 연속적으로 계산하지 않는다.
`generate` 는 `Supplier` 를 인수로 받아서 새로운 값을 생산한다.
```java
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
```
그렇다면 두 메서드의 가장 큰 차이점은 도대체 무엇일까?
바로 `Function` 과 `Supplier` 이다.
`iterate` 의 경우 `Function` 을 인자로 받고 `generate` 의 경우 `Supplier` 를 인자로 받는다.
`Suppler` 의 경우 상태가 없는 메서드이다.
꼭 상태가 없는 메서드만을 사용해야 하는 것은 아니다. 직접 `FunctionalInterface` `Supplier` 를 새롭게 만들어 상태를 갖고있다가 다음 값을 만들 때 상태를 고칠 수도 있다.
하지만, 벙렬 코드에서는 발행자에 상태가 있으면 안전하지 않다는 것을 강조해왔다.
스트림을 병렬로 처리하면서 올바른 결과를 얻으려면 **불변(immutable) 상태 기법**을 고수해야 한다.
따라서, 각 상황에 맞는 메서드를 통해 무한 스트림을 만드는 것이 올바르다.
Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/14