스트림 활용
1. Filtering
- T -> boolean
- 고유 요소 필터링 : distinct
List<Dish> dishes = menu.stream()
.filter(Dish::isVegetarian)
.collect(Collectors.toList());
List<Dish> dishes2 = menu.stream()
.filter(dish -> dish.getFat() < 400)
.distinct()
.collect(Collectors.toList());
filter는 기본적으로 반환값이 boolean이다.
- menu.stream(): 컬렉션을 스트림으로 변환
- filter(Dish::isVegetarian): dish클래스의 isVegetarian메서드를 메서드 참조하여 해당 조건을 필터링, 반환 = Stream(Dish)
- collect : 데이터 처리가 완료된 스트림을 리스트로 변경
2. Slicing
만약 리스트가 정렬된 요소이고, 특정 값 미만의 데이터들만 뽑아낸다고 하면, 굳이 끝까지 조건을 필터링하면서 찾을 필요가 있을까?
이렇게 나온것이 takewhile 과 dropwhile이다.
Fat이 순서대로 100, 200, 300, 400, 500이 있다 했을 때
takwWhile
List<Dish> result = specialMenu.stream()
.filter(dish-> dish.getFat() < 320)
.collect(Collectors.toList());
List<Dish> result2 = specialMenu.stream()
.takeWhile(dish-> dish.getFat() < 320) // 이거가 만족될때 까지만
.collect(Collectors.toList());
위에는 320미만의 값을 필터링 하는 것이므로, 전부 확인 후 100,200,300을 필터링 했을 것이다.
하지만 아래의 경우 400의 값을 확인할 때 아래의 필터에 만족하지 않기 때문에 더 이상 확인하지 않는다.
dropWhile
List<Dish> result3 = specialMenu.stream()
.dropWhile(dish-> dish.getFat() < 320) // 이거가 만족될때 까지만
.collect(Collectors.toList());
dropWhile은 반대로 조건문이 거짓이 될 때, 즉 fat이 400 이전에 검사했던 모든 값들을 버리고 나머지를 반환한다.
limit
List<Dish> result4 = specialMenu.stream()
.filter(dish-> dish.getFat() > 320)
.limit(3)
.collect(Collectors.toList());
limit의 경우 필터가 320보다 큰경우에서 해당 값을 만족하는 것이 3개까지만 수집하고 즉시 결과를 반환한다.
Skip
List<Dish> result5 = specialMenu.stream()
.filter(dish-> dish.getFat() > 320)
.skip(2)
.collect(Collectors.toList());
스트림이 filter를 충족하는 값 중에서 skip의 값 만큼 제외하고 나머지를 반환한다.
3. Mapping
매핑은 결국 스트림 내부의 값들을 변환 한다는 것이다.
List<String> words = Arrays.asList("Modern", "java", "in", "action");
List<Integer> wordsLength = words.stream()
.map(String::length)
.collect(Collectors.toList());
순서대로 보면
- stream은 String타입
- map을 통해서 Stream<Integer> (length반환형 = integer)
- stream을 수집하므로 결과적으로 List<Integer>가 나온다.
List<Integer> dishesLength2 = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
위와 같은 경우
- Stream<Dish>였지만
- getName으로 Stream<String>
- length로 Stream<Integer>로 변환되어 컬렉션으로 변경할 때 List<Integer>로 변경된 것을 볼 수 있다.
FlatMap: 스트림 평면화
위의 words 리스트를 보면 Modern, java, in, action 총 4개가 들어있습니다. 이를 하나의 리스트로 M,o....a,c,t,i,o,n처럼 각각의 문자를 분리하고 싶을때가 있는데 각 단어를 split("")해도 오류가 뜹니다.
이 이유가 String을 split하게 되면 String[]형식이 나오는데 이렇게 되면
Stream<String>에서 Stream<String[]>과 같이 변하는 것 뿐 하나의 리스트로 변형하지는 못합니다. 이때 이러한 값들을 하나로 묶어주는 역할을 하는것이 FlatMap이라 할 수 있습니다.
List<String> word = words.stream()
.map(w->w.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList());
- Stream<String>인 상태에서
- split()을 통해 Stream<String[]>가 되며
- 이를 평평하게 해주기 위해 flatMap(Arrays::stream)를 통해 Stream<String>으로 변환합니다.
- Arrays::stream의 경우 String[]를 Stream<String>으로 변환해 준다.
- 만약 map(Arrays::stream)을 하게 된다면 Stream<Stream<String>>이 되므로 원하는 답을 얻지 못한다.
4. 검색과 매칭
특정 데이터가 존재하는지를 검색하는 메서드들이 있다.
- anyMatch: 적어도 한 요소가 일치하는지 확인
- allMatch: 모든 요소가 일치하는지 확인
- noneMatch: 모든 요소가 일치하지 않는지 확인
위 방법들은 쇼트서킷 기법을 사용하는데, 쉽게 생각해서 &&에서 앞에 것이 0이 나오거나 ||에서 하나라도 1이 나오는 값이 있는 상태에서 더 이상 확인하지 않고 결과를 반환하는 기법을 말한다.
findAny
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findAny();
findAny는 filter를 만족하는 값을 임의로 반환하는 것이다.
findFirst
Optional<Dish> dish = menu.stream().filter(Dish::isVegetarian).findFirst();
findFirst는 filter를 만족하는 값 중 가장 앞에있는 값을 반환한다.
findAny vs findFirst
두 개가 나눠있는 이유는 stream의 병렬성 때문이다. 병렬 실행에서 첫 번째 요소를 찾는 것은 어렵기 때문에 두 메서드가 나눠져 있다.
5. Reducing
결과가 나올 때까지 스트림의 모든 요소를 반복적으로 처리하는 연산
요소의 합
int sum = Arrays.asList(1,2,3,4,5).stream().reduce(0, (a,b)->a+b);
초기 값이 없다면 아무 요소가 없을 때 반환할 수 없다. 따라서 이를 처리하기 위해 Optional을 사용
Optional<Integer> sum2 = Arrays.asList(1,2,3,4,5).stream().reduce((a,b)-> a+b);
최소/최대
Optional<Integer> max = Arrays.asList(1,2,3,4,5).stream().reduce(Integer::max);
int max2 = Arrays.asList(1,2,3,4,5).stream().reduce(0, (a,b)->a>b?a:b);
Optional<Integer> min = Arrays.asList(1,2,3,4,5).stream().reduce(Integer::min);
int min2 = Arrays.asList(1,2,3,4,5).stream().reduce(0, (a,b)->a>b?b:a);
스트림 연산 : 상태
스트림의 map, filter등은 입력 스트림에서 각 요소를 받아 결과를 출력 스트림에 보낸다. 이런 것을 내부 상태를 갖지 않는 연산이라 한다.
하지만 반대로 내부적으로 값을 가지고 있어서 값을 도출해야 하는 스트림도 있는데 reduce, sum, max와 같은 것들이다.
이러한 내부 값들은 한정되어 있으며, sorted나 distinct와 같이 모든 요소가 버퍼에 추가되어 있어야 하는 경우도 내부 상태를 갖는 연산이다.
6. 숫자형 스트림
reduce를 통한 계산을 하게 되면 결국 숫자 객체들을 언박싱을 해서 합을 하게 된ㄷ. 이러한 언박싱 비용을 줄여 효율적으로 처리하기 위해 기본형 특화 스트림을 제공한다.
- IntStream / DoubleStream, LongSream
- mapToInt / mapToDouble / mapToLong 등
int max = menu.stream().mapToInt(Dish::getFat).sum();
일반적인 map을 했다면 각 값들은 Integer로되어 있기 때문에 sum을 하면 비용이 들었다 하지만 mapToInt로 IntStream으로 변경 후의 합을 통해 효율적으로 처리가 가능하다.
숫자 범위
숫자 범위 또한 만들수 있는 메서드를 제공한다.
IntStream number = IntStream.rangeClosed(1,100); // 포함
IntStream number2 = IntStream.range(1,100); // 1과 100 포함X
7. 무한 스트림
스트림에서 크기가 고정되지 않는 스트림을 만들 수 있는데 이를 무한 스트림이라 한다. 이를 unbounded stream이라 하고
만드는 메서드는 iterate와 generate가 있다.
iterate
Stream.iterate(0, n -> n+2)
.limit(10)
.forEach(System.out::println);
generate
IntSupplier fib = new IntSupplier() {
private int previous= 0;
private int current = 1;
@Override
public int getAsInt() {
int oldPrevious = this.previous;
int nextValue = this.previous + this.current;
this.previous = this.current;
this.current = nextValue;
return oldPrevious;
}
};
IntStream.generate(fib).limit(10).forEach(System.out::println);
generate는 Supplier를 사용하기에 매번 새로운 값을 생성한다. 상태가 없어져야 하는 것은 아니며 상태를 고칠 수도있다.
iterate vs generate
- 둘다 무한 스트림을 만들 수 있다.
- iterate:
- 이전 상태의 값 기억하지 않는다.
- 따로 설정을 통해 이전 값을 기억하게 할 수 있다.
- generate:
- 이전 상태의 값 기억한다.
- 병렬 스트름에서 사용ㅎ라기 힘들다.
'JAVA > Java in action' 카테고리의 다른 글
[자바 인 액션] 6. 스트림 데이터 수집 (0) | 2023.03.23 |
---|---|
[자바 인 액션] 4. 스트림 (0) | 2023.03.23 |
[자바 인 액션] 3. 람다 표현식 (0) | 2023.03.23 |
[자바 인 액션] 2. 동작 파라미터화 (0) | 2023.03.22 |
[자바 인 액션] 1. 자바의 변화 (0) | 2023.03.22 |