이 장에서는 스트림 API가 지원하는 다양한 연산을 살펴본다.
이 장에서 살펴볼 스트림 활용법은 아래와 같습니다.
Stream<T> filter(Predicate<? super T> predicate);
filter 메서드는 boolean을 반환하는 함수인 프레디케이트를 인수로 받아서 true를 반환하는 모든 요소를 포함하는 스트림을 반환합니다.
List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0 ) //짝수인 경우 true를 반환하는 람다식을 삽입
.distinct() //중복 제거
.forEach(System.out::ptintln)
JAVA9에서 제공하는 takeWhile, dropWhile 메서드를 활용해 스트림을 슬라이싱 할 수 있습니다.
두 메서드는 인자로 들어오는 프레이케이드가 false값을 반환하는 최초 1회에 대해서 동작합니다. 그렇기 때문에, 정렬된 리스트에 대해서 좋은 성능을 보이게 됩니다.
//TakeWhile
List<Dish> slicedMenu1
= specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320) // 320칼로리 이상의 요리가 나오면, 그 전까지 있던 요리들까지만 챙김
.collect(toList());
//DropWhile
List<Dish> slicedMenu2
= specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320) // 320칼로리 이상의 요리가 나오면, 그 전까지 있던 요리들을 다 버림 (takeWhile과 정 반대)
.collect(toList());
limit, skip 메서드 역시 스트림을 슬라이싱 할 수 있는 메서드입니다.
limit(n)의 경우, 최대 n개의 요소까지 반환할 수 있으며,
skip(n)의 경우, n개의 요소를 제외한 후, n+1번 째 요소부터 반환하게 됩니다.
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3) //첫 세 요소를 선택한 후 즉시 결과 반환. 정렬되지 않아도 사용 가능
.collect(toList())
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2) //첫 두 요쇼 건너뛴 후 나머지 요리를 반환
.collect(toList())
스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
List<String> dishNames = menu.stream()
.map(Dish::getName) // Dish -> String 변환 함수 적용
.collect(toList());
위의 map 메서드는 Dish 오브젝트 -> String으로의 변환을 진행하므로, map 메서드의 출력 스트림은 Stream<string>
형식을 갖게 됩니다.
flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
List<String> uniqueCharacters =
words.stream()
.map(word -> word.split("")) //각 단어를 개별 문자열 배열로 전환 String -> String[]
.flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화 Stream<String[]> -> Stream<String>
.distinct()
.collect(toList());
위의 예제에서 .flatMap(Arrays::stream)
대신 .map(Arrays::stream)
이 들어간다면, 각 배열을 별도의 스트림으로 생성하게 됩니다.
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리로 자주 사용됩니다. 스트림 API 아래와 같은 다양한 유틸리티 메서드를 제공합니다.
아래의 메서드들 중에는 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있는 메서드들이 있습니다. 이를 통해 내부적으로 최적화를 수행할 수 있는데, 이를 쇼트서킷
기법이라고 합니다.
if(menu.stream().anyMatch(Dish::isVegetarian)) { //적어도 한 요소와 일치하는지 확인 - boolean을 반환하기 때문에, 최종 연산
...
}
boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000); //모든 요소가 주어진 프레디케이트와 일지하는지 검사
boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000); //주어진 프레디케이트와 일치하는 요소가 없는지 검사
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetrarian)
.findAny(); //현재 스트림에서 임의의 요소를 반환한다.(The behavior of this operation is explicitly nondeterministic)
모든 스트림 요소를 처리해서 값으로 도출하는 연산을 리듀싱 연산이라고 합니다.
map과 reduce 연산을 연결하는 기법을 맵 리듀스 패턴이라 하며, 쉽게 병렬화하는 특징이 있다고 합니다.
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a+b));
//간단한 맵리듀스 패턴
int count =
menu.stream()
.map(d -> 1)
.reduce(0, (a, b) -> a + b);
스트림에는 int같은 기본형이 Integer로 변환되어 들어가기 때문에, 박싱 비용이 존재합니다.
이와 같은 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림(primitive stream specialization)을 제공합니다.
//기존 코드
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0. Integer::sum) //int -> Integer로 변환하는 박싱 비용이 존재
// 기본형 특화 스트림 사용
int calories = menu.stream()
.mapToInt(Dish::getCalories) //IntStream을 반환한다.
.sum(); //Stream에선 제공하지 않는 sum 메서드를 IntStream에선 제공한다.
// 특화 스트림 -> 일반 스트림으로 변환 가능
Stream<Integer> stream = intStream.boxed();
// OptionalInt 등을 사용해 '결과값이 없는 경우'를 구분할 수 있다.
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); //maxCalories에 '결과값이 없는 경우' 기본 최대값을 명시적으로 설정하는 방법
일련의 값, 배열, 파일, 심지어 함수를 이용한 무한 스트림 만들기 등 다양한 방식으로 스트림을 만들 수 있습니다.
//값으로 스트림 만들기
Stream<String> stream = Stream.of("안녕", "하세요", "ㅎㅎㅎ");
// null이 될 수 있는 객체로 스트림 만들기 (JAVA9)
Stream<String> homeValueStream = homeValue == null ? Stream.empty() : Stream.of(value) // 기존방식(Stream.of에서 NPE를 발생시키는 듯? Stream.empty()를 이용하는 방식을 사용하는 것을 볼 수 있다)
Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home")) //JAVA9
// 배열로 스트림 만들기
int[] numbers = {2, 3, 5, 6};
int sum = Arrays.stream(numbers).sum();
// 파일로 스트림 만들기
Stream<string> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset()))
// 함수로 무한 스트림 만들기
Stream.iterate(0, n -> n + 2) //초기값, UnaryOperator<T>를 인자로 받아 무한정 스트림을 생성
.limit(10)
.forEach(System.out::println);
Stream.iterate(0, n -> n < 100, n -> n +4) //JAVA9에서는 두 번째 인자로 프레디케이트를 받아 언제까지 작업을 수행할 것인지 명시할 수 있다고 한다.
.forEach(System.out::println);
Stream.generate(Math::random) //generate는 Supplier<T>를 인수로 받아서 새로운 값을 생성한다.
.limit(5)
.forEach(System.out::println);