2022. 9. 4. 11:05ㆍ개발 관련 책 읽기/모던 자바 인 액션
✅ 아래 내용들에 대해서 알아보자
- 필터링, 슬라이싱, 매칭
- 검색, 매칭, 리듀싱
- 특정 범위의 숫자와 같은 숫자 스트림 사용하기
- 다중 소스로부터 스트림 만들기
- 무한 스트림
모든 실습 내용은 깃허브(아래 링크)에 있습니다. 참고 부탁드립니다 😀😀
필터링
1. 프레디케이트 필터링
아래의 코드는 프레디케이트(불리언을 반환하는 함수)를 인수로 받아서 채식주의자 음식 필터링하는 예제 코드이다.
여기서 주요하게 볼 코드는 filter를 사용하여 boolean Type을 반환하는 isVegetarian 메서드를 호출하여
채식주의자 음식만 필터링하는 부분이다.(아래 그림 1 참고)
@DisplayName("프레디케이트(불리언을 반환하는 함수)을 활용한 채식주의자 음식 필터링 하기")
@Test
public void FilterByVegetarianDishWithPredicate() {
//given
Collection<Dish> dishes = getDishList();
//when
List<Dish> vegetarianDishes = dishes.stream()
.filter(Dish::isVegetarian) //프레디케이트(불리언을 반환하는 함수) 필터링
.collect(toList());
//then
for (Dish vegetarianDish : vegetarianDishes) {
System.out.println("vegetarianDish = " + vegetarianDish);
}
assertThat(vegetarianDishes.size()).isGreaterThan(0);
}
2. 고유 요소 필터링
아래의 코드는 고유 요소로 이루어진 스트림을 filter와 distinct를 활용하여 짝수인 요소만 반환하는 코드이다.
@DisplayName("고유 요소로 이루어진 스트림 짝수 필터링")
@Test
public void filterEvenNumber() {
//given
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
//when
List<Integer> evenNumbers = numbers.stream()
.filter(num -> num % 2 == 0)
.distinct() //중복 제거
.collect(toList());
//중복 제거 된 짝수 출력
evenNumbers.stream()
.forEach(System.out::println);
//then
assertThat(evenNumbers.size()).isEqualTo(2);
}
스트림 슬라이싱
1.TakeWhile 활용
filter()는 모든 요소를 다 확인하여 조건에 대해 확인하지만
takeWhile()은 해당 조건에 대해 적합하지 않으면 연산을 중단하게 된다.
이러한 특징은 데이터 소스 양이 많을 때 O(n) 시간 복잡도를 다 돌지 않게 되어 연산 속도가 유리한 장점이 있다.
하지만 takeWhile()을 사용하기 위해서는 데이터 소스가 정렬된 상태 이어야 사용할 수 있는 조건이 있다.
/**
* Filter와 TakeWhile의 차이는 Filter는 모든 요소를 다 확인하지만 TakeWhile은 해당 요소가 조건에 대해 참이 아닐 경우 바로 거기서 연산을 멈춘다
* takewhile은 소스가 정렬되어 있을때 사용하기 좋음
*/
@DisplayName("TakeWhile을 활용한 칼로리 320이하인 요리 찾기")
@Test
public void findUnder320caloriesDishWithTakeWhile() {
//given
Collection<Dish> specialMenu = getSpecialMenu();
//when
List<Dish> under320CaloriesDishes = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() <= 320) //칼로리순으로 정렬되어있기 때문에 해당 요소가 320 칼로리가 넘으면 takewhile 연산은 종료한다. 시간 복잡도를 줄일 수 있는 장점이 생긴다
.collect(toList());
for (Dish dish : under320CaloriesDishes) {
System.out.println("dish = " + dish);
}
//then
assertThat(under320CaloriesDishes.size()).isEqualTo(2);
}
2.DropWhile 활용
dropWhile()은 takeWhile() 메서드와 정반대의 작업을 수행한다.
즉, 처음으로 거짓되는 지점이 발견되면 그 이전의 발견된 요소들을 버린 후 나머지 남은 요소들을 반환한다.
dropWhile()도 데이터 소스가 정렬된 상태이어야 한다는 전제조건이 있다.
/**
* DropWhile은 TakeWhile과 정반대의 작업을 수행한다. dropwhile은 프레디케이트가 거짓이 되면 작업을 중단하고 남은 요소를 반환한다
* dropWhile은 소스가 정렬되어 있을때 사용하기 좋음
*/
@DisplayName("DropWhile을 활용한 칼로리 320이상인 요리 찾기")
@Test
public void findOver320caloriesDishWithDropWhile() {
//given
Collection<Dish> specialMenu = getSpecialMenu();
//when
List<Dish> over320CaloriesDishes = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() <= 320) //칼로리가 320 이상인 음식이 나오면 작업 중단 후 남은 요소들을 반환한다
.collect(toList());
for (Dish dish : over320CaloriesDishes) {
System.out.println("dish = " + dish);
}
//then
assertThat(over320CaloriesDishes.size()).isEqualTo(3);
}
3. 스트림 축소
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는
limit(n) 메서드를 지원하여 최대 요소 n개를 포함하는 요소를 반환할 수 있다.
@DisplayName("스트림 축소")
@Test
public void streamReduction() {
//given
Collection<Dish> specialMenu = getSpecialMenu();
//when
List<Dish> over300CaloriesDishes = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 300)
.limit(3)
.collect(toList());
for (Dish over300CaloriesDish : over300CaloriesDishes) {
System.out.println("over300CaloriesDish = " + over300CaloriesDish);
}
//then
assertThat(over300CaloriesDishes.size()).isEqualTo(3);
}
4. 요소 건너뛰기
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.
limit(n)은 n개만큼만 요소를 반환하고 skip(n)은 n개를 제외한 요소들을 반환한다는 차이점이 있다.
@DisplayName("요소 건너뛰기")
@Test
public void streamSkip() {
//given
Collection<Dish> specialMenu = getSpecialMenu();
//when
List<Dish> skipDishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.collect(toList());
for (Dish skipDish : skipDishes) {
System.out.println("skipDish = " + skipDish);
}
//then
assertThat(skipDishes.size()).isEqualTo(1);
}
매핑
스트림 API의 map과 flatMap 메서드를 통해 특정 데이터를 선택하는 기능을 제공한다. 각각에 대해서 알아보자
1.Map
스트림은 함수를 인수로 받는 map을 지원한다.
인수로 제공된 함수는 각 요소에 적용되어 새로운 요소로 매핑된다.
다음 예시 1,2 를 보면서 이해해보자.
예시 1)
해당 문자열들의 글자수의 리스트를 반환하는 함수이다.
map을 활용하여 length 메서드를 인자로 넘겨 각 문자열들의 길이를 구하게 된다.
@DisplayName("각 단어가 포함하는 글자 수 반환 - 매핑 활용")
@Test
public void streamMapping() {
//given
List<String> words = Arrays.asList("Modern", "Java", "In", "Action");
//when
List<Integer> wordSize = words.stream()
.map(String::length)
.collect(toList());
for (Integer size : wordSize) {
System.out.println("size = " + size);
}
}
예시 2)
각 요리명의 길이를 map() + length()을 활용하여 리스트로 반환하는 코드이다.
@DisplayName("각 요리명의 길이를 반환")
@Test
public void getFoodNameLength() {
//given
Collection<Dish> dishes = getDishList();
//when
List<Integer> lengths = dishes.stream()
.map(dish -> dish.getName().length())
.collect(toList());
//then
for (Integer length : lengths) {
System.out.println("length = " + length);
}
}
=> 둘다 동일하게 모든 요소에 map() + length()를 적용하여 문자열의 길이를 구하였다.
2.flatMap
flatMap을 활용하여 ["Hello", "World"] 리스트를 ["H", "e", "l", "l", "o", "W", "r", "d"]를 포함하는 리스트로 변환해보자.
아래 예시 코드를 보면서 같이 확인해보자.
@DisplayName("스트림 평면화")
@Test
public void streamFlattening() {
//["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"] 결과값이 나오게 해야한다
//given
List<String> words = Arrays.asList("Hello", "World");
//1번째 시도
List<String[]> list = words.stream()
.map(s -> s.split(""))
.distinct()
.collect(toList());
list.forEach(strings -> strings.toString());
//2번째 시도
String[] arrayOfWords = {"Hello", "World"};
List<Stream<String>> collect = Arrays.stream(arrayOfWords)
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
//collect.forEach(System.out::println);
//3번째 시도(flatMap 사용)
List<String> result = words.stream()
.map(s -> s.split("")) //각 단어를 개별 문자로 tokenizing("h","e","l" ....)
.flatMap(strings -> Arrays.stream(strings)) //생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
result.forEach(System.out::println);
}
아래 그림은 위의 1번째 시도의 과정을 그림으로 나타낸것이다.
실제 결괏값을 collect메서드로 List<String>을 얻어야 하지만
List<String[]>을 얻어서 우리가 원하는 결괏값을 얻지 못하였다.
아래 그림은 map으로 먼저 각 문자열을 tokenizing 한 후
flatMap메서드를 사용하여 Stream<String[]> -> Stream<String>으로 변환한다.
그리고 distinct를 활용하여 중복제거 후 collect으로 List<String>을 얻어 우리가 원하는 결괏값을 도출한다.
여기서 핵심은 flatMap 메서드인데
flatmap은 각 값을 다른 스트림으로 만든 후 모든 스트림을 하나의 스트림으로 합치는 역할을 수행한다.
매칭
1.anyMatch
요소가 하나라도 일치하는지 확인한다. 아래는 음식들 중 채식요리가 적어도 하나라도 있는지 확인한다.
//요소 하나라도 일치하는지 확인(쇼트서킷)
@DisplayName("음식들 중에 채식요리가 적어도 하나라도 있는지 확인")
@Test
public void isVegetarainFoodAtLeastOne() {
//given
Collection<Dish> dishes = getDishList();
//when
boolean result = dishes.stream()
.anyMatch(Dish::isVegetarian); //boolean 반환하는 최종연산
//then
assertThat(result).isEqualTo(true);
}
2.allMatch
모든 요소가 일치하는지 확인한다. 아래 코드는 모든 음식이 1000칼로리 이하인지 확인한다.
//모든 요소 일치 확인(쇼트서킷)
@DisplayName("모든 음식이 1000칼로리 이하인지 확인")
@Test
public void isAllFoodUnder1000Kcal() {
//given
Collection<Dish> dishes = getDishList();
//when
boolean result = dishes.stream()
.allMatch(dish -> dish.getCalories() <= 1000);
//then
assertThat(result).isEqualTo(true);
}
3.noneMatch
nonematch는 Allmatch와 반대 연산을 수행한다.
즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
아래는 모든 음식이 1000칼로리 이상인지 확인하여 모든 음식이 1000칼로리 이하임을 증명한다.
//모든 요소 불일치 확인(쇼트서킷, <-> AllMatch와 반대 연산)
@DisplayName("모든 음식이 1000칼로리 이상인지 확인")
@Test
public void isAllFoodOver1000Kcal() {
//given
Collection<Dish> dishes = getDishList();
//when
boolean result = dishes.stream()
.noneMatch(dish -> dish.getCalories() >= 1000);
//then
assertThat(result).isEqualTo(true);
}
검색
1.findAny
findAny 메서드는 현재 스트림에서 임의의 요소를 Optional로 반환한다.
*Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.
자바에서 NPE(NullPointError)를 방지하고자 만든 클래스이다. 자세한 건 Optional에 대한 다른 글에서 설명하겠다.
@DisplayName("요소 검색")
@Test
public void ElementSearchByfindAny() {
//given
Collection<Dish> dishes = getDishList();
//when
dishes.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(System.out::println);
//then
}
2.findFirst
findFirst는 리스트 또는 정렬된 연속 데이터에서 첫 번째 요소를 찾기위한 메서드이다.
그러나 findFirst는 병렬 실행에서 첫 번째 요소를 찾기가 어렵다. 따라서 병렬 상황에서는 findAny를 사용한다.
/**
* findFirst와 findAny는 왜, 언제 사용하나?
* ->병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 반환 순서가 상관 없다면 Parallel Stream에서는 제약이 적은 findAny가 더 작합하다
* ->순차 실행에서는 findFirst를 활용하여 조건에 부합한 첫번째 요소를 찾을 때 유용하다
*/
@DisplayName("첫번째 요소 찾기")
@Test
public void findFirstElement() {
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 6);
//when
Integer result = list.stream()
.map(value -> value * value)
.filter(value -> value % 3 == 0)
.findFirst()
.orElse(0);
//then
assertThat(result).isEqualTo(9);
}
리듀싱
모든 요소들의 합을 구하려면 어떻게 해야할까?
바로 리듀싱(recude, 모든 스트림 요소를 처리해서 값으로 도출) 연산을 통해 합을 구할수 있다.
리듀싱을 사용 전/후 코드를 살펴보자
아래 코드는 리듀싱을 사용하지 않고 스트림의 모든 요소를 더하는 코드이다.
mapToInt로 스트림의 모든 요소를 int로 변환 후 sum 메소드를 사용하여 합을 구하였다.
@DisplayName("리듀싱을 적용하기 전 리스트의 모든 요소 더하기")
@Test
public void sumWithStreamSumMethod() {
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//when
int sum = list.stream()
.mapToInt(Integer::intValue)
.sum();
System.out.println("sum = " + sum);
//then
assertThat(sum).isEqualTo(15);
}
리듀싱을 적용한 코드를 보자.
리듀싱 적용하기 전 코드에 비해 코드가 짧고 가독성이 좋아보인다.
reduce(0, (a,b) -> a+b)) 람다 코드가 초기값 0을 받아 스트림의 각 요소에 누적으로 합을하여 실제 결과값을 구한다.(0+4+5+3+9 ....)
자세한건 아래 그림을 참고하자.
/**
* 리듀싱
* -> 모든 스트림 요소를 처리해서 값으로 도출하는 과정
* -> FP에서는 종이를 작은 조각이 될 떄까지 반복해서 접는 것과 비슿해서 "폴드"라 부름
*/
@DisplayName("리듀싱을 적용 후 리스트의 모든 요소 더하기")
@Test
public void sumWithReducing() {
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//when
/**
* 파라미터 = 초기값, accumulator(누산기,람다)
* (1+2) +3) +4) +5) 이런식으로 누적으로 계산되서 마지막 스트림 요소까지 연산한다
*/
Integer sum = list.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("sum = " + sum);
//then
assertThat(sum).isEqualTo(15);
}
초기값이 없는 recduce 메서느는 Optional 객체를 반환한다.
왜 Optional 객체를 반환할까? 그 이유는, Stream에 아무 요소도 없는 경우 초기값이 없으므로 reduce는 연산을 할 수없다. 그로 인해 Optional 객체로 감싼 결과를 반환한다.
@DisplayName("초기값이 없는 리듀싱을 적용 후 리스트의 모든 요소 더하기")
@Test
public void sumWithReducingNotInitValue() {
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//when
Integer sum = list.stream()
.reduce((a, b) -> a + b)//초기값이 없는 reduce는 Optional을 반환함
.orElse(0);
System.out.println("sum = " + sum);
//then
assertThat(sum).isEqualTo(15);
}
Reduce를 황용하여 최댓값/최소값을 구하는 코드를 보자.
recude() 메서드의 람다식에 Math.max(),min()를 적용하여 최대값/최소값을 구한다.
자세한건 아래 그림을 참고하자
@DisplayName("Reduce를 활용한 최대값 구하기")
@Test
public void getMaxValueWithReduce(){
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//when
Integer maxValue = list.stream()
.reduce(Math::max)
.orElse(0);
//then
assertThat(maxValue).isEqualTo(5);
}
@DisplayName("Reduce를 활용한 최소값 구하기")
@Test
public void getMinValueWithReduce(){
//given
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//when
Integer minValue = list.stream()
.reduce(Math::min)
.orElse(0);
//then
assertThat(minValue).isEqualTo(1);
}
5장은 내용이 많아서 1,2편으로 나누려고 합니다.
2편 보기 => 2022.11.08 - [개발자 책 읽기/모던 자바 인 액션] - Chapter 5 - 스트림 활용(2편)
'개발 관련 책 읽기 > 모던 자바 인 액션' 카테고리의 다른 글
Chapter 7 - 병렬 데이터 처리와 성능 (0) | 2022.11.07 |
---|---|
Chapter 6 - 스트림으로 데이터 수집 (0) | 2022.09.25 |
Chapter 4 - 스트림 소개 (0) | 2022.08.22 |
Chapter 3 - 람다 표현식 (0) | 2022.08.22 |
Chapter 2 - 동작 파라미터화 코드 전달하기 (0) | 2022.08.22 |