FeGwan-Training / FeGwan

0 stars 0 forks source link

Chapter5 스트림 활용 #15

Closed MyeoungDev closed 1 year ago

MyeoungDev commented 1 year ago

Discussed in https://github.com/FeGwan-Training/FeGwan/discussions/14

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) 상태 기법**을 고수해야 한다. 따라서, 각 상황에 맞는 메서드를 통해 무한 스트림을 만드는 것이 올바르다.
HoFe-U commented 1 year ago

수고하셨습니다 :D 중간에 특화 스트림이 있는데 특화 스트림 종류중하나인 OptionalInt 같은 경우에는 어떨때 쓰이는건지 궁금하네요!! 또한 "상태가 없는 메서드" 와 "상태가 있는 메서드" 이부분이 중간연산자와 최종연산자랑 연관이있는건가요?