2025. 2. 14. 17:51ㆍBook/모던 자바 인 액션
목차.
1. 필터링, 슬라이싱, 매칭
2. 검색, 매칭, 리듀싱
1. 필터링
1.1 프리디케이트 필터링
스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트(불리언을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
List<Dish> vegetarianMenu = DishExample.menu.stream()
.filter(Dish::isVegetarian)
.toList();
1.2 고유 요소 필터링
스트림은 공유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.(고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다) 예를 들어 다음 코드는 리스트의 모든 짝수를 선택하고 중복을 필터링한다.
List<Integer> numbers = Arrays.asList(1, 2, 2, 3, 3, 3, 4, 5, 2, 2, 5, 1);
numbers.stream()
.filter(n -> n % 2 == 0)
.distinct()
.forEach(System.out::println);
>> 2, 4
//숫자 스트림
1, 2, 2, 3, 3, 3, 4, 5, 2, 2, 5, 1
//filter(n -> n % 2 == 0)
2 2 4 2 2
//distinct()
2 4
//forEach(System.out::println)
System.out.println(2);
System.out.println(4);
2. 스트림 슬라이싱
2.1 프레디케이트를 이용한 슬라이싱
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다.
TAKEWHILE 활용
다음과 같은 특별한 요리 목록을 가지고 있다고 가정한다.
new Dish("season fruits", 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("salmon", false, 450, Dish.Type.FISH),
new Dish("french fries", true, 530, Dish.Type.OTHER),
new Dish("pizza", true, 550, Dish.Type.OTHER),
new Dish("beef", false, 700, Dish.Type.MEAT),
new Dish("pork", false, 800, Dish.Type.MEAT)
320칼로리 이하의 요리를 선택한다고 하면 앞에서 사용한 filter를 다음처럼 이용할 수 있을 것이다.
List<Dish> filteredMenu = DishExample.menu.stream()
.filter(dish -> dish.getCalories() < 320)
.toList();
위 리스트는 자세히 보면 칼로리 순으로 정렬이 되어있다. filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용하게 된다. 따라서 리스트가 이미 정렬되어 있다는 사실을 이용해 320칼로리보다 크거나 같은 요리가 나왔을 때 반복 작업을 중단할 수 있다. takeWhile을 이용하면 무한 스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스할 수 있다.
//요소가 칼로리 순으로 정렬되어 있다면 takeWhile을 이용해 320보다 작은 칼로리를 가진 요소를 반복 작업을 중단할 수 있다.
List<Dish> slicedMenuWithTakeWhile = DishExample.menu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.toList();
DROPWHILE 활용
dropWhile은 takeWhile과 반대로 나머지 요소를 선택한다. 즉 320칼로리보다 큰 요소를 탐색할 때 적절히 쓰일 수 있다. dropWhile은 프레디케이트가 처음으로 기짓이 되는 지점까지 발견된 요소를 버리고 남은 모든 요소를 반환한다.
//요소가 칼로리 순으로 정렬되어 있다면 takeWhile을 이용해 320보다 큰 칼로리를 가진 요소만 반복 작업을 할 수 있다.
List<Dish> slicedMenuWithDropWhile = DishExample.menu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.toList();
2.2 스트림 축소
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어있으면 최대 요소 n개를 반환할 수 있다. 아래 예제를 보자 300칼로리 이상의 세 요리르 ㄹ선택해서 리스트를 만들 수 있다.
List<Dish> dishes = DishExample.menu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.toList();
//메뉴 스트림
fish meat rice chicken salmon pizza
//filter(dish -> dish.getCalories() > 300)
meat rice salmon
//limit(3)
meat rice salmon
//toList()
meat rice salmon
2.3 요소 건너뛰기
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다. limit(n) 과 skip(n)은 상호 보완적이 ㄴ연산을 수행한다. 예를 들면 300칼로리 이상의 처음 두 요리를 건너뛴 다음에 300칼로리가 넘는 나머지 요리를 반환한다.
List<Dish> dishes = DishExample.menu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.toList();
//메뉴 스트림
fish meat rice chicken salmon pizza
//filter(dish -> dish.getCalories() > 300)
meat rice salmon pizza
//skip(2)
salmon pizza
//toList()
salmon pizza
3. 스트림 매핑
3.1 스트림의 각 요소에 함수 적용하기
스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다. List<Dish> 리스트에서 스트림을 추출해 name 멤버변수를 호출하는 함수를 인수로 전달하고 List<String>으로 매핑하는 예제를 보자.
//메서드 참조를 통해 요리 이름을 추출해 리스트로 만들기
List<String> dishNames = DishExample.menu.stream()
.map(Dish::getName)
.toList();
//메서드 참조를 통해 요리 이름을 추출 후 이름 길이를 구해 리스트로 만들기
List<Integer> dishNamesLength = DishExample.menu.stream()
.map(Dish::getName)
.map(String::length)
.toList();
3.2 스트림 평면화
메서드 map을 이요해서 리스트의 각 단어의 길이를 반환하는 방법을 확인했으면 이를 응용해서 리스트에서 고유 문자로 이루어진 리스트를 반환해보자. 예를 들어 ["Hello", "World"] 리스트가 있다면 결과로 ["H", "e", "l", "o", "W", "r", "d"]를 포함하는 리스트가 반환되어야 한다.
리스트에 있는 각 단어를 문자로 매핑한 다음 distinct로 중복된 문자를 필터링해 간단하게 풀 수 있을 것 같지만 그렇지 않다.
/*
아래는 Stream<String[]> 를 리스트로 반환하는 잘못된 코드이다.
List<String> flattenStrings = strings.stream()
.map(s -> s.split(""))
.distinct()
.toList();
*/
위 코드에서 map으로 전달한 람다는 각 단어의 String[]을 반환한다. 따라서 map 메소드가 반환한 스트림의 형식은 Stream<String[]>이다.
flatMap 활용
flatMap을 사용하면 다음처럼 문제를 해결할 수 있다.
//map 사용
List<String> uniqueCharacters = strings.stream()
.map(word -> word.split("")) //각 단어를 개별 문자를 포함하는 배열로 변환 Stream<String[]>
.map(Arrays::stream) //문자열 배열에서 스트림을 뽑아낸다 Stream<Stream<String>
.distinct()
.toList();
//flatMap 사용
List<String> uniqueCharacters = strings.stream()
.map(word -> word.split("")) //각 단어를 개별 문자를 포함하는 배열로 변환 Stream<String[]>
.flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화 Stream<String>
.distinct()
.toList();
flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.
4. 검색과 매칭
1 프레디케이트가 적어도 한 요소와 일치하는지 확인
anyMatch
if (DishExample.menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is Vegetarian");
}
2. 프레디케이트가 모든 요소와 일치하는지 검사
allMatch
모든 요소가 주어진 프레디케이트와 일치하는지 검사
boolean isHealthy = DishExample.menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
noneMatch
주어진 프레디케이트와 일치하는 요소가 없는지 확인
boolean isHealthy = DishExample.menu.stream()
.noneMatch(dish -> dish.getCalories() > 1000);
3. 요소 검색
findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용할 수 있다.
Optional<Dish> dish =
DishExample.menu.stream()
.filter(Dish::isVegetarian)
.findAny();
스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다. 즉 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.
4. 첫 번째 요소 찾기
리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾아보자
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst();
* findFirst와 findAny 메서드는 얼핏 비슷해보이는데 언제 사용해야할까?
- 두 메서드가 필요한 이유는 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
5. 리듀싱
지금까지 살펴본 최종 연산은 불리언(allMatch, anyMatch, noneMatch ...), void(forEach), Optional(findAny) 를 반환했다. 또한 collect(toList())로 모든 스트림의 요소를 리스트로 모으는 방법도 봤다.
리듀스 연산을 이용하면
- '메뉴의 모든 칼로리의 합계를 구하시오'
- '메뉴에서 가장 칼로리가 가장 높은 요리는?'
과 같이 스트림 요소를 조합해서 더 복잡한 질의를 표현할 수 있다.
이러한 질의를 수행하려면 Integer 같은 결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리해야 한다. 이런 질의를 리듀싱 연산(모든 스트림 요소를 처리해서 값으로 도출하는)이라고 한다.
5.1 요소의 합
아래 for-each 루프를 이용해서 리스트의 숫자 요소를 더하는 코드를 확인하자.
//for-each
int sum = 0;
for (int x : numbers) {
sum += x;
}
numbers의 각 요소는 결과에 반복적으로 더해진다. 코드에는 파라미터를 두 개 사용했다.
애플리케이션의 반복된 패턴을 추상화는 reduce를 이용하면 위 코드를 복붙하지 않고 모든 숫자를 더하거나 곱하는 연산을 구현할 수 있다.
sum = numbers.stream().reduce(0, (a, b) -> a + b);
reduce는 두 개의 인수를 가진다.
- 초깃값 0
- 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>, 예제에서는 람다 표현식 (a, b) -> a + b를 사용했다.
reduce로 다른 람다 (a, b) -> a * b 를 넘겨주면 모든 요소에 곱셈을 적용할 수 있다.
mul = numbers.stream().reduce(1, (a, b) -> a * b);
스트림이 하나의 값으로 줄어들 때까지 람다는 각 요소를 반복해서 조합한다.
reduce(0, (a, b) -> a + b)
4 | 5 | 3 | 9 | |
0 | + | |||
4 | + | |||
9 | + | |||
12 | + | |||
21 |
우선 람다의 첫 번째 파라미터 (a)에 0이 사용되었고, 스트림에서 4를 소비해서 두 번째 파라미터 (b)로 사용했다.
0+4의 결과인 4가 누적값이 되었다.. 이제 누적값으로 람다를 다시 호출하며 다음 요소인 5를 소비한다. 이런식으로 마지막 요소까지 호출하며 21이 도출된다.
초깃값이 없는 reduce도 있고 이 reduce는 Optional 객체를 반환한다.
5.2 최댓값과 최솟값
최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
Optional<Integer> min = numbers.stream()
.reduce(Integer::min);
'Book > 모던 자바 인 액션' 카테고리의 다른 글
CHAPTER6 - 스트림으로 데이터 수집(1) (0) | 2025.02.21 |
---|---|
CHAPTER5 - 스트림 활용(2) (0) | 2025.02.17 |
CHAPTER4 - 스트림 (1) | 2025.02.12 |
CHAPTER3 - 람다 표현식 (0) | 2025.01.16 |
CHAPTER2 - 동작 파라미터화 코드 전달 (0) | 2025.01.07 |