모던 자바 인 액션 5장
모던 자바 인 액션 5장 : 스트림 활용
4장에서 스트림에 대한 간략한 개념 및 특징을 알아보았고 어떻게 스트림이 구성되어있는지 살펴보았다.
5장은 스트림 활용으로 직접 코드를 작성해보며 어떤 상황에 스트림을 적용하면 좋을지 체득할 수 있을것 같다.
5.1 필터링
프레디케이트 필터링 방법과 고유 요소만 필터링하는 방법을 알아보자
5.5.1 프레디케이트로 필터링
- 프레디케이트는 인수를 받아 boolean을 반환
- 즉, 프레디케이트와 일치하는 요소를 포함하는 스트림을 반환한다.
List<Dish> vegetarianMenu = menu.stream() .filter(Dish::isVegitarian) .collect(toList());
5.5.2 고유 요소 필터링
스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct 메서드도 지원한다.(SQL의 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)
5.2 스트림 슬라이싱
여기서는 스트림의 요소를 선택하거나 스킵하는 다양한 방법을 설명한다.
5.2.1 프레디케이트를 이용한 슬라이싱
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다.
먼저 코드를 보면 쉽게 이해할 수 있을것이다.
List<Dish> slicedMenu1
= specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
System.out.println(slicedMenu1);
takeWhile은 내부 predicate가 처음으로 false를 반환하는 곳에서 반복을 멈춘다.
List<Dish> slicedMenu2
= specialMenu.stream()
.dropWhile(dish -> dish.getCalories() > 320)
.collect(toList());
System.out.println(slicedMenu1);
dropWhile은 takeWhile과 완전히 반대이다. 처음으로 true를 반환하는 곳에서 반복을 멈춘다.
5.2.2 스트림 축소
스트림 축소는 앞서 살펴본 limit(n) 메서드로 제공된다.
(앞에서 충분히 이해하였으므로 스킵)
5.2.3 요소 건너뛰기
처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());
위 코드는 처음 두 요리를 건너뛴 다음 300칼로리가 넘는 나머지 요리를 반환한다.
만약 처음 등장하는 두 고기 요리를 필터링하는 코드를 짜려면 어떻게 해야할까?
List<Dish> dishes = menu.stream()
.filter(d -> d.getType() == Dish.Type.MEAT)
.limit(2)
.collect(toList());
직관적이고 순차적으로 코드를 짤 수있다.
5.3 매핑
특정 객체에서 특정 데이터를 선택하는 작업을 매핑이라고 한다. SQL의 테이블에서 특정 열만 선택하는 것과 동일하다.
5.3.1 스트림의 각 요소에 함수 적용하기
스트림은 함수를 인수로 받는 map 메서드를 지원한다.
인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());
위 코드는 스트림의 요리명을 출력한다.
List<String> words = Arrays.asList("Modern","Java", "In","Action");
List<Integer> wordLength = words.stream()
.map(String::length)
.collect(toList());
위 코드는 단어 목록에 있는 단어들의 글자 수를 담은 리스트를 만든다.
확실히 스트림을 이해하고 나니 for문을 통해 외부 반복을 수행하는 것 보다 직관적이고 코드 길이가 짧아져서 좋은것 같다.
또한 map을 연결해서 요리명의 길이를 추출할 수도 있다.
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
5.3.2 스트림 평면화
map을 이용해서 리스트에서 고유 문자로 이루어진 리스트를 반환하려면 아래와 같이 코드를 작성할 수 있을것이다.
words.stream()
.map(String::split(""))
.distinct()
.collect(toList());
그러나 위 map으로 전달한 람다는 각 단어의 String[]을 반환한다는 점이 문제다. 위 map 메소드가 반환한 스트림 형식은 Stream<String[]>이다.
우리가 원하는것은 Stream
map과 Arrays.stream 활용
Arrays.stream() 배열 내부 요소에 하나씩 접근한다.
따라서 위 코드를 다음과 같이 수정하여 해결할 수 있다.
words.stream()
.map(String::split("")) //각 단어를 개별 문자로 포함하는 배열로 변환
.map(Array::stream) //각 배열을 별도의 스트림으로 생성
.distinct()
.collect(toList());
flatMap 사용
flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, map(Array::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.
List<String> uniqueCharacters =
word.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
아래 그림은 flatMap을 이용해서 단어 리스트에서 고유 문자 찾기 과정을 보여준다.

5.4 검색과 매칭
특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리방법으로 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny등 다양한 유틸리티 메서드를 제공한다.
5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
anyMatch를 이용하면 된다.
if(menu.stream().anyMatch(Dish::isVegitarian)) {
System.out.println("The menu is vegitarian friendly!!");
}
- anyMatch는 boolean을 반환하므로 최종 연산이다.
5.4.2 프레디케이트가 모든 요소와 일치하는지 검사
allMatch 메서드는 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.
boolean isHealthy = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
noneMatch
noneMatch는 allMatch와 반대로 일치하는 요소가 없는지 확인한다. 위 예제와 아래는 완전히 똑같다.
boolean isHealthy = menu.stream().
.nonMatch(dish -> dish.getCalories() >= 1000);
Short-Circuit
숏서킷은 전체 조건식을 다 평가하지 않고 앞의 조건만으로 최종 결과가 정해지면 그 즉시 결과를 반환한다.
allMatch, noneMatch, findFirst, findAny 등의 연산은 숏서킷 효과가 있다.
5.4.3 요소 검색
findAny 메서드를 통해 임의의 요소를 찾을 수 있다.
Optinal<Dish> dish = menu.stream()
.filter(Dish::isVegitarian)
.findAny();
Optional
Optianl
- isPresent() : 값이 있으면 true, 없으면 false를 반환
- ifPresent(Consumer
block) : 값이 있으면 주어진 블록을 실행한다.
(Consumer : T -> void) - T get() : 값이 존재하면 반환, 값이 없으면 NoSuchElementException을 일으킴
- T orElse(T other) : 값이 있으면 값을 반환, 없으면 기본값을 반환
5.4.4 첫 번째 요소 찾기
List<Integer> someNumbers = Arrays.asList(1,2,3,4,5);
Optional<Integer> firstSqureDivisibleByThree =
someNumbers.stream()
.map(n -> n*n)
.filter(i -> i%3 == 0)
.findFirst();
findFirst와 findAny는 언제 사용?, 병렬 실행에서는 첫 번째 요소를 찾기 어렵기 때문에 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
5.5 리듀싱
리듀싱 연산이란 결과가 나올때 까지 스트림의 모든 요소를 반복적으로 처리하는 것이다.
5.5.1 요소의 합
아래 코드는 숫자 요소의 합을 구하는 코드이다.
int sum=0;
for(int x:numbers){
sum+=x;
}
numbers의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지 reduce 과정을 반복한다.
reduce를 이용하면 애플리케이션의 반복된 연산을 추상화할 수 있다.
int sum = numbers.stream().reduce(0, (a,b) -> a+b);
reduce는 두 개의 인수를 갖는다.
- 초깃값 0
- 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator
초깃값 없음
초깃값을 받지 않도록 오버로드된 reduce도 있다.
그러나 이 reduce는 Optional 객체를 반환한다.
스트림에 아무 요소도 없을 수 있기 때문이다.
5.5.2 최댓값과 최솟값
요소의 최댓값과 최솟값을 찾을땐 어떻게할까?
reduce는 두 인수를 받는다
- 초깃값
- 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다
위에서는 람다로 덧셈을 수행하였지만 여기서는 두 요소에서 최댓값만 반환하는 람다만 있으면 될 것이다.
Optional<Integer> max = numbers.stream().reduce(Integer::max);
5.6절은 실전문제로 연습하는 부분이므로 스킵합니다.
5.7 숫자형 스트림
5.7.1 기본형 특화 스트림
스트림 API는 박싱 비용을 피할 수 있도록 아래 3가지 스트림을 제공한다.
- IntStream : int 요소에 특화
- DoubleStream : double 요소에 특화
- LongStream : long 요소에 특화
각각의 인터페이스는 sum, max같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.
특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련 있으며 스트림에 추가 기능을 제공하지는 않는다는 사실을 알아두자.
숫자 스트림으로 매핑
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
- mapToInt, mapToDouble, mapToLong 세 가지 메서드가 존재한다.
- map과 같은 기능을 수행하지만 Stream
이 아닌 특화 스트림을 반환한다.
객체 스트림으로 복원하기
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.
기본값 : OptionalInt
합의 예제에서는 0이라는 기본값이 있었다.
하지만 IntStream에서 최댓값을 찾을 때는 0이라는 기본값 때문에 잘못된 결과가 도출될 수 있다.
스트림에 요소가 없는 상황과 실제 최댓값이 0인 상황을 어떻게 구분할 수 있을까?
이때 Optional도 기본형 형식이 존재한다.
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
5.7.2 숫자 범위
특정 숫자 범위의 숫자를 처리할때 유용한 메서드를 제공한다.
IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
IntStream evenNumbers = IntStream.rangeClosed(1, 100)
.filter(n -> n%2 ==0);
System.out.println(evenNumbers.count());
5.8 스트림 만들기
5.8.1 값으로 스트림 만들기
Stream.of()를 이용해서 임의의 수를 인수로 받아 스트림을 만들 수 있다.
Stream<String> stream = Stream.of("Modern","Java","In,"Action");
stream.map(String::UpperCase).forEach(System.out::println);
함수로 무한 스트림 만들기
스트림 API는 함수에서 스트림을 만들 수 있는 두 정적 메서드 Stream.iterate와 Stream.generate를 제공한다.
이 메서드로 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과 달리 크기가 고정되지 않은 스트림을 만들 수 있다.
iterate와 generate에서 만든 스트림은 요청할 때마다 주어진 함수를 이용해서 값을 만든다.
보통 무한한 값을 출력하지 않도록 limit(n)과 함께 사용한다.
iterate 메서드
Stream.iterate(0, n -> n+2)
.limit(10)
.forEach(System.out::println);
- 초깃값(0)과 람다를 인수로 받는다.
- 람다의 결과에 의존해서 순차적으로 연산을 수행한다.
또한 자바 9의 iterate 메서드는 두 번째 인수로 프레디케이트를 받아 언제까지 작업을 수행할 것인지 기준으로 사용할 수 있다.
IntStream.iterate(0, n->n<100, n -> n+4)
.forEach(System.out::println);
takeWhile을 이용하면 위와 같은 결과를 얻는 코드를 만들 수 있다.
IntStream.iterate(0, n-> n+4)
.takeWhile(n -> n<100)
.forEach(System.out::println);
generate 메서드
generate는 iterate와 달리 생산된 값을 연속적으로 계산하지 않는다.
generate는 Supplier
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println)
- 나중에 사용할 어떤값도 저장해두지 않는, 즉 상태가없는 메서드이다.
- 나중에 상태를 가지면 생기는 부작용에 대하여 설명이 나온다고한다.
5.9 마치며
- 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
- filter, distinct, takeWhile, dropWhile, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
- 소스가 정렬되어 있다는 사실을 알고 있을때 takeWhile과 dropWhile 메서드를 효과적으로 사용할 수 있다.
- map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
- findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드로 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
- 이들 메서드는 숏서킷, 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
- reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를들어 reduce로 스트림의 최댓값이나 모든 요소의 합계를 계산할 수 있다.
- filter, map 등은 상태를 저장하지 않는 stateless operation이다. reduce같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기에 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 상태 있는 연산이라고 부른다.
- IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
- 컬렉션뿐 아니라 값, 배열, 파일, iterate와 generate같은 메서드로도 스틀미을 만들 수 있다.
- 무한한 개수의 요소를 가진 스트림을 무한 스트림이라 한다.