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)
.distinct()
.forEach(System.out::println);
스트림 슬라이싱은 자바 9에서 도입된 새 기술이며, 스트림 슬라이싱을 이용하면 아래와 같은 이점을 누릴 수 있다.
- 프레디케이트를 이용하는 방법
- 스트림의 요소를 선택하거나 스킵하는 다양한 방법
- 스트림의 처음 몇 개의 요소를 무시하는 방법
- 특정 크기로 스트림을 줄이는 방법
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile
두 가지 새로운 메서드를 지원한다.
List<Dish> specialMenu = Arrray.asList(
new Dish("seasonal fruit", true, 12, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
...
);
위 리스트는 이미 칼로리 순으로 정렬되어 있다.
만약 320 칼로리 이하의 요리를 선택하려면 filter가 떠오를 것이다.
따라서 320 칼로리보다 크거나 같은 요리가 나왔을 때 반복작업을 중단 할 수 있는데, takeWhile
을 사용하면된다.
아주 많은 요소를 포함하는 큰 스트림에서는 이 차이가 상당히 커질 수 있다.
List<Dish> slicedMenu1
= specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
만약 나머지 요소(320 칼로리보다 큰 것들)를 선택하려면 dropWhile
을 이용하면 된다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지
발견된 요소를 버린다.
List<Dish> slicedMenu2
= specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
스트림은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n)
메서드를 지원한다.
스트림은 처음 n개 요소를 제외한 스트림을 반환
하는 skip(n)
메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈
스트림이 반환된다.
스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다. (특정 객체에서 특정 데이터를 선택하는 작업
)
map과 flatMap은 변환에 가까운 매핑
스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
(이 과정은 기존 값을 고친다(modify) 라는 개념보다는 새로운 버전을 만든다
개념에 가까우므로 변환(transforming)에 가까운 매핑(mapping)
이라는
단어를 사용한다.)
- 요리명 추출
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, 하나의 평면화된 스트림을 반환
한다.
List<String> uniqueCharacters =
words.stream()
.map(word -> word.splict("")) // 각 단어를 개별 문자를 포함하는 배열로 반환
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
- 숫자 1 2 3 4 5가 제공되면 해당 숫자의 제곱근을 반환 1 4 9 16 25
List<Integer> numbers = Arrays.asList(1,2,3,4,5);
List<Integer> squars = numbers.stream()
.map(n -> n * n)
.collect(toList());
- 두 개의 숫자 리스트 1,2,3과 3,4 가 있을때 모든 숫자 쌍의 리스트를 반환 하시오 (1,3) (1,4) (2,3) ...
List<Integer> numbers1 = Arrays.asList(1, 2, 3);
List<Integer> numbers2 = Arrays.asList(3, 4);
List<int[]> pairs = numbers1.stream()
.flatMap(i -> numbers2.stream().map(j -> new int[]{i, j}))
.collect(toList());
- anyMatch 메서드 사용
if(menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is somewhat vegetarian friendly!!");
}
- allMatch 메서드 사용
boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000);
- noneMatch 메서드 사용
boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000);
anyMatch, allMatch, noneMatch 세 메서드는 스트림
쇼트서킷
기법, 즉 자바의 &&, ||와 같은 연산을 활용한다.
예를 들어 여러 and 연산으로 연결된 커다란 불리언 표현식을 평가한다고 가정하자. 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이 전체 결과도 거짓이 된다. 이러한 상황을 쇼트서킷이라고 부른다. 스트림의 allMatch, noneMatch, findFirst, findAny, limit 등의 연산은 모든 스트림의 요소를 처리하지 않고도 반환할 수 있다. 즉, 원하는 요소를찾았으면 즉시 결과를 반환할 수 있다.
- findAny
- 스트림에서 임의의 요소를 반환한다.
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
Optional<T>
클래스는 값으 존재나 부재 여부를 표현하는 컨테이너 클래스이다. 만약 findAny()가 null을 반환한다면 쉽게 에러를 일으킬 수 있다.
이때 Optional에서 제공하는 ifPresent(Consumer<T> block)
을 사용할 수 있다.
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny()
.ifPresent(dish -> System.out.println(dish.gtName()); // 값이 있으면 반환하고 없으면 아무일도 일어나지 않는다.
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
.map(n -> n * n)
.filter(n -> n % 3 == 0)
.findFirst(); //9
findFirst와 findAny의 메서드가 모두 필요한 이유는 병렬성
때문이다. 병렬 실행에서는 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가
상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
리듀싱 연산이란 모든 스트림 요소를 처리해서 값으로 도출하는 연산을 의미한다. 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이를
작은 조각이 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)
라고 부른다.
- for-each 루프를 사용해서 리듀싱하기
int sum = 0;
for(int x : numbers) {
sum += x;
}
리스트에서 숫자가 하나 남을 때까지 reduce 과정을 반복한다.
- reduce 사용하기
int sum = numbers.stream().reduce(0, (a,b) -> a+b);
reduce는 두 개의 인수를 갖는다.
- 초기값 0
- 두 요소를 조합해서 새로운 값을 만드는
BinaryOperator<T>
// 숫자리스트 : 8,4,1,9,7,5
reduce(0, (a, b) -> a + b)
- 위 처럼 되어있을 경우 초기값 0이 a의 자리에 들어가고, b에는 8이 들어간다.
- 누적값이 8이 되었다.
- 누적값 8이 a에 들어가고, b에는 4가 들어간다.
- 누적값이 12가 되었다.
- 누적값 12가 a에 들어가고, b에는 1이 들어간다.
- 누적값이 13이 되었다.
- 반복...
메서드 참조로 간결하게 코드를 수정하면 아래와 같다.
int sum = numbers.stream().reduce(0, Integer::sum);
초기값을 받지 않도록 오버로드된 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));
Optional을 반환하는 이유는 스트림에 아무 요소도 없는 경우 reduce가 합계를 반환할 수 없기 때문이다.
map과 reduce를 연결하는 기법을 맵 리듀스 패턴
이라고 한다. 쉽게 병렬화하는 특징 덕분에 구글이 웹 검색에 적용하면서 유명해졌다.
int count = menu.stream()
.map(d -> 1)
.reduce(0, (a,b) -> a + b);
- 추출 : map
- 선택 : filter
- reduce를 이용한 스트림 요소의 합 구하기
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
위 과정에서는 내부적으로 합계를 계산하기 위해서 Integer가 int형으로 언박싱 된다. 따라서 박싱 비용이 소모된다.
자바 8에서는 세 가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 int 요소에 특화된 IntStream
, double 요소에 특화된
DoubleStream
, long 요소에 특화된 LongStream
을 제공한다.
각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.
스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세가지 메서드를 가장 많이 사용한다.
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
IntStream intStream = menu.strema().mapToInt(Dish::getCalories); // 스트림을 숫자 스트림으로 변환
Stream<Integer> stream = intStream.boxed(); // 숫자 스트림을 스트림으로 변환
자바 8의 IntStream과 LongSteam에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
IntSream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
rangeClose(1, 100)은 1과 100을 포함하며 range(1, 100)은 1과 100을 제외한다.
임의의 수를 인수로 받는 정적 메서드 Stream.of
를 이용해서 스트림을 만들 수 있다.
- Stream.of로 문자열 스트림을 만드는 예제
Stream<String> stream = Stream.of("Mordern", "Java", "In", "Action");
stream.map(String::toUpperCase).forEach(System.out.println);
Arrays.stream
을 이용해서 배열을 인수로 받아 스트림을 만들 수 있다.
int[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();
스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다. 두 연산을 이용해서 무한 스트림(infinite stream)
즉, 언바운드 스트림(unbounded stream)
을 만들 수 있다.
- 피보나치수열
0, 1, 1, 2, 3, 5, 8 ...
Stream.iterate(new int[]{0, 1}, t-> new int[] {t[1], t[0] + t[1]})
.limit(10)
.map(t -> t[0)
.forEach(System.out::println);
- generate 메서드
generate는 iterate와 달리 생산된 각 값을 연속적으로 계산하지는 않는다.
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println)