데이터를 어떻게 처리할지는 스트림 API가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다. 따라서 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다. 스트림 API는 내부 반복 뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현하는 외부 반복으로는 달성할 수 없다.
filter 메서드는 프레디케이트를 인수로 받아서 일치하는 모든 요소를 포함하는 스트림을 반환한다.
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다. 고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정된다.
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0) //2, 2, 4
.distinct() //중복 제거
.forEach(System.out::println); //2, 4 출력
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다. 칼로리가 오름차순으로 정렬된 리스트가 있을 때 특정 칼로리 이하의 요리를 스트림으로 나타낸다고 할 때 다음의 코드는 올바른 결과를 나타내지만 정렬이 돼있기 때문에 어느 시점부터는 반복 작업이 불필요해진다.
List<Dish> filteredMenu = specialMenu.stream()
.filter(dish -> dish.getCalories() < 320) //전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용
.collect(toList());
이때 takeWhile 연산을 이용하면 도중에 반복 작업을 중단해 스트림을 슬라이스할 수 있다.
List<Dish> sliceMenu1 = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
나머지 요소를 선택하려면 dropWhile을 이용할 수 있다. dropWhile은 takeWhile과 정반대의 작업을 수행하며 프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. 그리고 무한 스트림에서도 동작한다.
List<Dish> sliceMenu2 = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 다음 코드는 프레디케이트와 일치하는 처음 세 요소를 선택한 다음 즉시 결과를 반환한다.
List<Dish> dishes = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.
List<Dish> dishes2 = menu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(2)
.collect(toList());
특정 객체에서 특정 데이터를 선택하는 데이터 처리 과정에서 자주 수행되는 연산이다. 스트림 API의 map과 flatMap 메서드가 그 기능을 제공한다.
스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName) //Stream<String>
.map(String::length) //Stream<Integer>
.collect(toList());
["Hello", "Wolrd"]란 리스트를 고유 문자로 이루어진 리스트로 반환해본다. 결과값은 ["H", "e", "l", "o", "W", "r", "d"]이 된다.
List<String> words = Arrays.asList("Hello", "World");
배열을 문자열 스트림으로 반환하기 위해 Arrays.stream() 메서드를 이용한다. 하지만 이렇게 코드를 작성할 경우 각 배열을 스트림으로 만들기 때문에 결국 스트림 리스트가 반환된다.
List<Stream<String>> wrongResult = words.stream()
.map(word -> word.split("")) //각 단어를 문자열 배열로 반환
.map(Arrays::stream) //각 배열을 별도의 스트림으로 생성
.distinct()
.collect(toList());
flatMap을 사용하면 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, 각 스트림을 하나의 평면화된 스트림으로 반환한다.
List<String> uniqueCharacters = words.stream()
.map(word -> word.split("")) //각 단어를 문자열 배열로 반환
.flatMap(Arrays::stream) //생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
다음 코드는 flatMap을 이용하여 두 숫자 리스트를 모든 숫자 쌍의 리스트를 반환한다.
List<Integer> list1 = Arrays.asList(1, 2, 3);
List<Integer> list2 = Arrays.asList(3, 4);
List<int[]> result = list1.stream()
.flatMap(i -> list2.stream()
.map(j -> new int[]{i, j})
)
.collect(toList());
boolean isVegetarian = menu.stream()
.anyMatch(Dish::isVegetarian);
boolean isHealthy = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
noneMatch는 allMatch와 반대 연산을 수행한다. isHealthy를 다음과 같이 구현할 수 있다.
boolean isHealthy = menu.stream()
.noneMatch(dish -> dish.getCalories() >= 1000);
anyMatch, allMatch, noneMatch 세 메서드는 스트림 쇼트서킷 기법, 즉 자바의 &&, ||와 같은 연산을 활용한다. 이러한 연산은 모든 스트림의 요소를 처리하지 않고도 결과를 반환할 수 있다. 원하는 요소를 찾았으면 즉시 결과를 반환한다. 마찬가지로 limit도 해당되며 무한한 요소를 가진 스트림을 유한한 크기로 줄일 수 있는 유용한 연산이다.
finyAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. 위의 findAny는 아무 요소도 반환하지 않을 수 있는데 null은 쉽게 에러를 일으킬 수 있으므로 Optional을 이용한다.
menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(dish -> System.out.println(dish.getName()));
리스트 또는 정렬된 연속 데이터로부터 생성된 스트림처럼 일부 스트림에는 논리적인 아이템 순서가 정해져 있을 수 있다. 이런 스트림에서 첫 번째 요소를 찾기 위해 findFirst() 메서드를 사용할 수 있다.
Optional<Integer> firstSquareDivisibleByThree =
oneToFive.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst(); //9
findFirst와 findAny 메서드가 필요한 이유는 병렬성 때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출하는 연산이다.
List<Integer> numbers = List.of(4, 5, 3, 9);
int sum = 0;
for (int number : numbers) {
sum += number;
}
위와 같이 numbers 배열의 합을 구하는 for-each 코드가 있을 때 이를 reduce를 이용해서 표현할 수 있다.
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
초깃값은 0이 되고 람다의 첫 번째 a에 0이 사용되고 b에서 4이 사용되어 0+4=4이 새로운 누적값이 된다. 누적값으로 람다를 다시 호출하여 다음 요소인 5를 소비하여 결과는 9이 된다. 이런 식으로 마지막 요소를 호출할 때까지 반복한다.
메서드 참조를 이용하면 코드를 좀 더 간결하게 만들 수 있다.
int sum = numbers.stream()
.reduce(0, Integer::sum);
초깃값을 받지 않도록 오버로드된 reduce도 있는데 이때는 Optional 객체를 반환한다.
Optional<Integer> reduce = numbers.stream()
.reduce(Integer::sum);
reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행하므로 최댓값과 최솟값도 찾을 수 있다.
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
map과 reduce를 연결하는 기법을 map-reduce 패턴이라 하며, 쉽게 병렬화하는 특징이 있다.
int count = menu.stream()
.map(dish -> 1)
.reduce(0, Integer::sum);
count() 메서드와 동일한 결과를 가진다.
long count = menu.stream()
.count();