[Java] 스트림 연산
스트림에는 여러가지 중간 연산, 최종 연산이 있다. 한 번 살펴보자.
필터링
Predicate로 필터링
스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 Predicate를 인수로 받아서 Predicate와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
예를 들어, 다음과 같이 액션 영화만 필터링하여 모을 수 있다.
List<Movie> actionMovies = movies.stream()
.filter(Movie::isActionGenre)
.collect(Collectors.toList());
고유 요소로 필터링
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.
예를 들어, 다음은 모든 짝수를 선택하고 중복을 필터링하는 코드이다.
List<Integer> numbers = Arrays.asList(1, 1, 2, 3, 4, 4, 4, 5);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach(System.out::println);
슬라이싱
스트림에는 요소를 선택하서나 스킵하는 여러 방법이 있다. 한 번 살펴보자.
Predicate를 이용한 슬라이싱
Java 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다.
takeWhile
만약 다음처럼 정렬되어 있는 요소가 있다고 할 때, filter 연산을 이용하면 전체 스트림을 반복하면서 각 요소에 Predicate를 적용하게 된다.
List<Integer> numbers = Arrays.asList(1, 1, 2, 3, 4, 4, 4, 5);
numbers.stream()
.filter(i -> i < 3)
.collect(Collectors.toList());
하지만, takeWhile을 적용하면 크거나 같은 수가 나왔을 때 반복 작업을 중단할 수 있다.
numbers.stream()
.takeWhile(i -> i < 3)
.collect(Collectors.toList());
dropWhile
dropwhile은 takeWhile과 정반대 작업을 수행한다. 즉, 커음으로 거짓이 되는 지점까지 발견된 요소를 버린다.
Predicate가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다.
numbers.stream()
.dropWhile(i -> i < 3)
.collect(Collectors.toList());
스트림 축소
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.
예를 들어, 다음처럼 12000원 이하의 세 영화를 선택하여 리스트로 만들 수 있다.
List<Movie> selectedMovies = movies.stream()
.filter(movie -> movie.getFee() > 12000)
.limit(3)
.collect(Collectors.toList());
위 코드는 Predicate와 일치하는 처음 세 요소를 선택한 다음 즉시 결과를 반환한다. 만약 소스가 정렬되어 있지 않았다면 limit의 결과도 정렬되지 않은 상태로 반환된다.
요소 건너뛰기
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. 만약 n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.
예를 들어, 다음은 처음 3개를 건너뛴 후 12000원 이하의 영화를 반환한다.
List<Movie> selectedMovies = movies.stream()
.filter(movie -> movie.getFee() > 12000)
.skip(3)
.collect(Collectors.toList());
매핑
스트림은 Function을 인수로 받는 map 메서드를 지원한다.
인수로 제공된 Function은 각 요소에 적용되며, Function을 적용한 결과가 새로운 요소로 매핑된다.
예를 들어, 다음은 영화 제목을 매핑하는 코드이다.
List<String> movieNames = movie.stream()
.map(Movie::getTitle)
.collect(Collectors.toList());
또, map은 다음과 같이 연결할 수도 있다.
List<String> movieNames = movie.stream()
.map(Movie::getTitle)
.map(String::length)
.collect(Collectors.toList());
스트림 평면화
만약, ["Hello", "World"] 리스트의 각 요소를 추출하고 중복을 제거하여 ["H", "e,", "l", "o", "W", "r", "d"] 이와 같이 추출하려면 어떻게 해야될까?
다음과 같이 하면 추출할 수 있을까?
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(Collectors.toList());
아쉽게도 위와 같이하면 우리가 원하는 결과를 얻을 수 없다. 왜냐하면 map에서 split 메서드를 호출하여 반환된 스트림은, 각 리스트의 요소들이 [["H", "e", "l", "l", "o"], ["W", "o", "r", "l", "d"]] 이렇게 나뉜 Stream<String[]> 형태이기 때문이다.
때문에 여기서 distinct 수행 시 ["H", "e", "l", "l", "o"]와 ["W", "o", "r", "l", "d"]는 중복이 아니기 때문에 그대로 나오게 된다.
즉, 결과는 List<String[]>으로 나오게 된다.
때문에 위 요구사항은 flatMap 메서드를 통해 해결할 수 있다.
List<String> characters = words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(Collectors.toList());
만약 flatMap이 아니라 그냥 map을 사용했다면 List<Stream<String>>과 같은 형태로 반환될 것이다. 결과는 직접 확인해보자.
여기서 Arrays.stream()은 다음과 같이 배열을 스트림으로 만드는 메서드이다.
String[] words = {"Hello", "World"};
Stream<String> streamOfWords = Arrays.stream(words);
검색과 매칭
스트림 API는 특정 속성이 데이터 집합에 있는지 여부를 검색할 수 있는 다양한 메서드를 제공한다.
Predicate가 적어도 한 요소와 일치하는지 확인
Predicate가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 anyMatch 메서드를 이용한다.
boolean hasActionMovie = movies.stream()
.anyMatch(Movie::isActionGenre);
anyMatch는 boolean을 반환하기 때문에 최종 연산이다.
Predicate가 모든 요소와 일치하는지 검사
allMatch 메서드를 통해 스트림의 모든 요소가 주어진 Predicate와 일치하는지 검사한다.
boolean isAllActionMovie = movies.stream()
.allMatch(Movie::isActionGenre);
noneMatch
noneMatch는 allMatch와 반대 연산을 수행한다. 즉, 주어진 Predicate와 일치하는 요소가 없는지 확인한다.
boolean hasNotActionMovie = movies.stream()
.noneMatch(Movie::isActionGenre);
anyMatch, allMatch, noneMatch 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.
쇼트서킷?
표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 되는 상황을 쇼트서킷이라고 한다.
anyMatch, allMatch, noneMatch, findFirst, findAny 등 연산은 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있다.
요소 검색
findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
Optional<Movie> movie = movies.stream()
.filter(Movie::isActionGenre)
.findAny();
스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다. 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.
첫 번째 요소 찾기
리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다.
이런 스트림에서 첫 번째 요소를 찾으려면 다음과 같이 findFirst 메서드를 활용한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
numbers.stream()
.map(number -> number * number)
.filter(number -> number % 3 == 0)
.findFirst();
findFirst vs findAny
findFirst와 findAny는 모두 요소를 하나씩 찾는 메서드이다. 그럼 둘의 차이는 없을까?
두 메서드는 병렬 실행에서 차이가 발생한다. 병렬 실행 상황에서는 첫 번째 요소를 찾기 어렵기 때문에, 요소의 반환 순서가 없다면 병렬 스트림에서 제약이 적은 findAny를 사용하는 것이 좋다.
리듀싱
스트림은 모든 스트림 요소를 처리해서 값으로 도출하는 리듀싱 연산을 지원한다.
요소의 합, 곱
reduce를 이용해 요소의 합을 구할 수 있다.
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
앞의 0은 초깃값을 의미한다.
또, 다음과 같이 요소의 곱도 구할 수 있다.
int product = numbers.stream().reduce(1, (a, b) -> a * b);
reduce 연산은 스트림의 값이 하나의 값으로 줄어들 때 까지 각 요소를 반복해서 조합한다.
초깃값이 없을 경우 다음과 같이 Optional 객체를 반환한다.
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
최댓값, 최솟값
최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
'Language > Java' 카테고리의 다른 글
[Java] Java 8 특징 (0) | 2023.04.13 |
---|---|
[Java] 숫자형 스트림 (0) | 2023.04.11 |
[Java] HttpServlet (0) | 2023.04.06 |
[Java] 스트림(Stream) (0) | 2023.04.06 |
[Java] 메서드 참조 (0) | 2023.04.04 |
댓글
이 글 공유하기
다른 글
-
[Java] Java 8 특징
[Java] Java 8 특징
2023.04.13 -
[Java] 숫자형 스트림
[Java] 숫자형 스트림
2023.04.11 -
[Java] HttpServlet
[Java] HttpServlet
2023.04.06 -
[Java] 스트림(Stream)
[Java] 스트림(Stream)
2023.04.06