1. 스트림의 활용
4장에서는 스트림의 기본 개념, 기초에 대해 알아봤다면
5장은 스트림을 활용하는 심화과정으로 생각해 볼 수 있다.
- 필터링,슬라이싱,매핑,검색,매칭,리듀싱 등등…. 수많은 반복적인/복잡한 작업을 스트림으로 가독성 좋게 코드를 작성할 수 있다.
2. 스트림 활용
2.1. 필터링
말 그대로 필터링을 거치는 활용법이다.
filter() 메소드가 이 역할을 하는 스트림 메소드인데, 프리디케이트를 인수로 받아서, 프리디케이트와 일치하는 모든 요소를 포함하는 스트림을 리턴해 준다.
다음 예제는 필터링 + distinct 를 통해 중복 제거까지 수행하는 코드이다.
List<Integer> numbers = Arrays.asList(1,2,1,3,3,2,4);
numbers.stream()
.filter(i -> i%2 == 0)
.distinct()
.forEach(System.out::println);

뭔가 보면 볼수록 쿼리랑 비슷하다. 명령형이 아닌 ‘선언형’ 방식이 이렇게 편하다.
2.2. 슬라이싱
2.2.1. takeWhile(), dropWhile()
슬라이싱은, 필터링,매핑만큼 자주는 아니지만 그래도 꼭 필요한 상황이 생기기 마련.
takeWhile(), dropWhile(), limit(), skip() 등이 있다.
repository로부터 수백,수천개의 샘플 데이터들을 추출하였는데, 정작 원하는 데이터의 개수는 10개 남짓인 경우가 항상 있다.
- ‘수백,수천개 데이터 중 일부만 랜덤으로 뽑아주세요’, ‘노출은 10개이긴 한데, 새로고침 할때마다 새로운 데이터가 보였음 싶네요’
이런 요구사항들을 들어줄 때가 반드시 있다.ㅋㅋ
List<Dish> specialMenu = Arrays.asList(
new Dish("sesonal fruit", true, 120, Dish.Type.OTHER),
new Dish("prawns", false, 300, Dish.Type.FISH),
new Dish("rice", true, 350, Dish.Type.OTHER),
new Dish("chicken", false, 400, Dish.Type.MEAT),
new Dish("french fries", true, 530, Dish.Type.OTHER),
);
각 Dish의 3번째 요소가 칼로리라고 치자.
320칼로리 이하의 저칼로리 음식을 뽑아야 하는 요구조건이 존재한다고 하자.
잘 보면 저 스페셜 메뉴가 칼로리 순으로 정렬되어 있음을 알 수 있다.
스트림의 filter() 메소드를 통해 5개의 요소를 다 프리디케이트를 통해 값을 비교해도 되지만, 특별히 소팅이 되어있거나 하는 ‘특수한 상황’ 에서는 takeWhile() 이나 dropWhile() 같은 메소드를 이용할 수 있다.
이 메소드를 사용하는 이유는 성능을 위함인데, 예제는 5개로만 한정되어있지만, 실제 비즈니스에서는 저런 리스트가 수백,수천개로 담겨져 올 수 있는 경우가 부지기수이기에….
List<Dish> slicedMenu1 = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
상기 적어놓은 코드를 동작시키면, filter(dish -> dish.getCalories() < 320) 과 같은 결과를 내놓지만, 320칼로리 이상의 메뉴(예제의 4,5번째 요소)는 스트림에서 검사 자체를 하지 않는다. 당연히 성능면에서 filter() 메소드보다는 빠르겠지.
List<Dish> slicedMenu1 = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
상기 적어놓은 코드를 동작시키면, 정확히 takeWhile()의 반대의 작업을 수행한다. 320칼로리 이상의 메뉴를 찾는 순간, 나머지 모든 요소를 리턴해 준다.(당연히 나머지 모든 요소에 대해 검사를 하지 않기에, 검사 횟수는 takeWhile() 메소드와 동일하다.)
2.2.2. limit(), skip()
뭐… 굳이 설명을 더 하지 않아도 직관적으로 파악 가능한 메소드이다.
List<Dish> limitMenu = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
위 코드는 filter() 메소드 내에 있는 프리디케이트를(300칼로리 초과) 3개 찾는 순간 스트림이 종료된다.
주의: 정렬과는 상관없이, 콜렉션의 첫 요소부터 탐색해서 프리디케이트 매칭되는 요소 3개만 찾고 그 다음은 거들떠도 보지 않는다.
List<Dish> skipMenu = specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.skip(3)
.collect(toList());
위 코드는 300칼로리를 초과하는 메뉴를 3개 찾는 순간, 그 이후 모든 스트림을 반환한다.
예를 들어 칼로리가 1000, 100, 700, 50, 70, 800, 900, 450….. 의 순서로 메뉴 리스트가 존재할 시에
6번째 요소(800칼로리)가 ‘300칼로리는 초과하는 두번째 메뉴’이기에, 6번째 요소인 900칼로리 메뉴부터 그 이후의 것들을 리턴한다.
(중간의 100,50,70 칼로리 메뉴까지 무시된다는 점 명심하자!)
limit() 메소드의 상호 보완이라고 생각하면 된다. (limit(n) + skip(n) –> 원본 리스트가 나온다….)
2.3. 매핑
컬렉션(대표적으로 리스트) 의 타입이 보통 primitive data type 을 wrapping한 wrapper class인 경우가 많은데, 그렇지 않은 경우도 매우 많다.
여러가지(갯수,종류) 프로퍼티가 존재하는 개발자가 직접 구현한 클래스 타입의 경우, 이 요소들 중 내가 필요한 값들만 추려서 보고 싶은 경우가 있다.
- 예제의 Dish에서 칼로리 리스트들만 추출하고 싶은 경우
- String타입의 리스트에서, 각 프로퍼티의 문자열 길이 등
이럴 경우 매핑을 많이 사용한다.
정확한 비유일지는 모르겠지만 선형대수에서의 정사영을 생각하면 조금 이해가 쉽지 않을까..
List<String> words = Arrays.asList("Modern","Java","In","Action");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(toList());
위 코드는 words의 각 프로퍼티의 ‘문자열 길이’ 가 리턴되는 코드이다.
2.3.1. map vs flatmap
‘리스트에서 고유 문자’로 이루어진 리스트를 반환하고 싶은 케이스를 생각해보자
- [“Hello”, ‘World”] 리스트 –> [“H”, “e”, ” l”, “o”, “W”, “r”, “d”] 를 추출
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
위 코드를 실행하면?
- [“H”, “e”, “l”, “l”, o”], [“W”, “o”, “r”, “l”, “d”] 가 나온다.ㅋㅋ

그림을 보면 알겠지만 단순히 map메소드는 원본 콜렉션의 각 프로퍼티들을 구분해서 로직을 수행한다
- “Hello” 를 split한 결과값과, “World”를 split한 결과값을 ‘별개의 스트림’으로 동작을 수행시킴.
- 스트림이 두개~!
그럼 하나의 스트림으로 만들어 볼까?
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream)
.distinct()
.collect(toList());
위 코드를 실행하면? 똑같다-_-;
“” 로 쪼갠 값을 다시 별도의 stream으로 만들어봤자, Stream<Stream<String>> 타입으로 만들어지기 때문임….
앞서 말했듯, 단순히 map으로는 무슨 짓을 해도 별개의 스트림을 하나의 스트림으로 flatting(평면화) 시킬 수 없기 때문임.
그래서!
이런 문제에 봉착했을 때(모든 프로퍼티들을 ‘하나의 스트림’으로 만들고자) 바로 flatMap을 사용하면 된다 이말이야
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream)
.distinct()
.collect(toList());

이제 map과, flatMap의 차이를 알 수 있겠다!
2.4. 검색과 매칭
특정 속성이 프로퍼티 내에 있는지 여부를 검색할 수 있는것도 stream에서 지원해준다.
2.4.1. anyMatch
프리디케이트가 적어도 한 요소와 일치하는지를 확인할 수 있는 메소드
if (menu.stream().anyMatch(Dish::isVegetarian)) {
// do something.
}
위 코드는 메뉴중에 vegetarian속성이 ‘단 하나라도 있으면’ true를 리턴한다.
2.4.2. allMatch
프리디케이트가 ‘모두’ 일치하는지를 확인할 수 있는 메소드
if (menu.stream().allMatch(Dish::isLowCalories)) { // less than 1000kCal.
// do something.
}
위 코드는 메뉴들이 ‘모두 다’ 저칼로리인 경우라면 true를 리턴한다
2.4.3. noneMatch
allMatch 메소드와 반대되는 연산.
if (menu.stream().allMatch(Dish::isLowCalories)) { // less than 1000kCal.
// do something.
}
if (menu.stream().noneMatch(Dish::isHighCalories)) {
// do something.
}
위 두개의 if코드블럭은 같은 로직이다.
2.4.4. findAny
스트림에서 임의의 요소를 반환
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
위 코드를 수행하면 채식요리 중 아무 요리나 하나 리턴이 된다.
뭐….책에서는 여기서 Optional타입으로 findAny결과값을 받긴 했는데, Optional에 대한 설명은 슈퍼패스
2.4.5. findFirst
리스트, 소팅된 데이터로부터 처음 찾게 되는 값을 리턴해 준다.
List<Integer> someNumber = Arrays.asList(1,2,3,4,5);
someNumber.stream()
.map(n -> n*n)
.filter(n -> n%3 == 0)
.findFirst(); //9
위 코드(3으로 나누어 떨어지는 첫번째 제곱값)를 수행하면 나오는 제일 첫번째 요소를 리턴한다.
findFirst이므로, 배열의 순서에 영향을 받으며, 위 예제의 순서가 6,1,2,3,4,5 로 배열이 되어있다면 3이 아니라 3 앞에 있는 0번지값이 ‘6’이 리턴된다.
참고
2.4.1. ~ 2.4.5. 다섯개의 메소드 모두 다 ‘쇼트서킷’ 기법으로 배열의 요소를 탐색한다고 한다.
쇼트서킷이란..
- 그냥 true/false 가 ‘확정’ 되는 타이밍에, 더 볼 필요 없는 다음 요소들은 안본다. 이거임.
- 5판 3선승제에서 초반 3연승 확정하면 4,5차전 안하는 개념.
2.5. 리듀싱
지금까지는 필터링,매핑 등등 리턴 타입이 boolean티입인 결과들에 대해서만 알아 보았다.
하지만 실제 비즈니스에서는 이런 요구뿐만 아니라 콜렉션의 모든 합을 구해야 하는 sum이나, 최대값인 max를 구해야 하는 등의 요구가 얼마든지 있을 수 있다.
이 때 사용하는 방법이 스트림의 reduce메소드를 사용하는 것임.
2.5.1. sum
레거시 자바에서는 다음과 같이 콜렉션의 모든 요소의 합을 구하기 위해서 다음과 같은 for문(for-each) 를 사용하였다.
int sum = 0;
for (int x : numbers) {
sum += x;
}
이 코드를 잘 보면, 사용되는 주요 요소들은 다음과 같다
- 초기값 (sum=0)
- 연산자 (+)
이 요소들을 염두에 둔 채로, 스트림의 reduce 메소드를 이용하면 다음과 같이 이용할 수 있다.
int sum = numbers.stream().reduce(0, (a,b) -> a+b);
reduce메소드의 첫번째 파라미터로 초기값 0을 명시하고, 가상의 값인 a와 b(정확히는 스트림이 반복되면서 a에는 이전까지 수행했던 결과값이 치환, b에는 이번에 수행할 값이 치환)를 ‘더하는(+)’ 연산자를 명시하게 되었다.
글로는 이해가 잘 안갈 수 있으니, 무슨 이야기냐 하면….

여기서 a+b 가 아닌, 더 복잡한 연산(곱셈이거나, 특정 수식….) 을 넣으면 그 행위를 (당연히)그대~로 수행한다
참고
그런데 저 간단한 덧셈의 경우, 람다식을 더 간단히 표현할 수 있는 방법이 있었다. ‘메소드 참조’.
Integer라는 wrapper class에 sum이라는 static메소드가 존재하니, 메소드 참조를 이용하면 가독성 좋게 이런 코드로 표현할 수도 있다!
(모던 자바의 시초?인 자바 8에서부터 추가됨)
int sum = numbers.stream().reduce(0, Integer::sum);
2.5.2. min & max
하나. 2.5.1장에서, 콜렉션의 연산을 리듀싱하기 위해서는 다음 두개의 요소만 파악하면 된다고 하였다.
- 초기값
- 연산자
둘. 그 연산자를 wrapper class에 있는 static메소드로 메소드 참조를 하는것까지 살펴보았다.
그리고….
셋. Integer라는 wrapper class에는 sum뿐만 아니라, max()와 min()이라는 static메소드도 존재한다.
이 하나,둘,셋을 합쳐 생각해보면…
‘최대값,최소값을 구할 수 있는 reduce 메소드 코드를 작성할 수 있다’ 가 되시겠다.
- 초기값 : 개발자가 정하고 싶은 값 아무거나…뭐 0이 될수도 있고, 무한대가 될수도 있고..
- 연산자 : Integer.max()
int max = numbers.stream().reduce(0, Integer::max); //또는 초기값을 넣지 않은, Optional을 리턴하는 결과값을 받을수도 있다 Optional<Integer> max = numbers.stream().reduce(Integer::max);
뭐 이렇게…..

2.6. 숫자형 스트림
지겹게 보던 메뉴예제에서, 여러 메뉴의 총 칼로리 합을 구하고 싶다고 하자.
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
정상 작동할 것이다.
근데, 내부적으로 박싱 비용(primitive data type –> wrapper class)이 들어간다. getCalories의 타입인 int가 Integer로 변환되어야 하는 오버헤드가 콜렉션의 매 요소마다 들어가게 됨.
그러면 reduce를 쓰지 않고, 곧바로 sum을 하면 어떨까
int calories = menu.stream()
.map(Dish::getCalories) //return Stream!!!!
.sum();
map의 리턴 타입이 스트림 타입이기 떄문에 곧바로 sum 할 수가 없다.
그래서 기본형 특화 스트림이라는게 존재한다.
int calories = menu.stream()
.mapToInt(Dish::getCalories) //return IntStream!!!!
.sum();
그러면, mapToInt메소드를 통해 리턴되는 IntStream을 다시 Stream타입으로 변환시킬 수 있을까?
IntStream intStream = menu.stream().mapToInt(Dish::getCalories); // return IntStream Stream<Integer> stream = intStream.boxed(); //IntStream --> boxed()를 통해 --> Stream<integer>
이렇게 하면 숫자형 스트림을 다시 원래의 Stream타입으로 변경할 수 있다.
2.7. 스트림 만들기
컬렉션을 통해 스트림을 사용한 예제는 수없이 봤는데, 스트림을 직접 만드는 것도 당연히 가능하다
2.7.1 값을 스트림으로 만들기
간단하다. Stream.of로 만들 수 있다.
Stream<String> stream = Stream.of("Hello","World");
stream.map(String::toUpperCase);
뭐 이런 식으로…..ㅇㅇ
2.7.2 nullable한 객체를 스트림으로 만들기
간단하다2. Stream.ofNullable 을 이용하면 된다.
String homeValue = System.getProperty("home");
Stream<String> homeStream = homeValue == null ? Stream.empty() : Stream.of(value); // 안좋은 예
Stream<String> homeStream2 = Stream.ofNullable(homeValue); // 이렇게 써라
2.7.3 배열을 스트림으로 만들기
간단하다3. Arrays.stream을 이용한다
int sum = Arrays.stream({2,3,4,5,6}).sum();
2.8. 무한 스트림
스트림이 무한으로 만들어질 수도 있다
무한으로 만들어졌다고 해서 무한으로 쓰지 말자….
Stream.iterate(0, n -> n+2)
.limit(10);
이런 식으로 수행 횟수를 제한하거나 하는게 좋다.
Stream.iterate(0, n -> n<100, n->n+4)
.forEach(System.out::println);
이런 식으로 iterate메소드에 파라미터가 3개 있는 경우는 2번째 파라미터가 중단조건에 해당한다(n이 100보다 커지면 종료시킨다.)
마무리
- 스트림 API를 잘~ 사용하면 복잡한 데이터처리를 쉽게 할 수 있다!
- 필터링
- filter, distinct
- 슬라이싱
- takeWhile, dropWhile, skip, limit
- 원본이 정렬되어있다는 것을 알 때 takeWhile, dropWhile 사용 지향!
- 매핑
- map, flatMap
- flatMap은 각 프로퍼티들이 또 다른 콜렉션(2차원 배열이나, 문자열을 캐릭터로 쪼개서 봐야할 때)일 때 사용하면 유용함
- 검색
- findFirst, findAny
- 리듀싱
- reduce
- filter, map 등은 상태를 저장하지 않은 ‘상태 없는 연산’이다. reduce,sorted,distinct는 반대로 상태를 저장하는(합을 구해야 하고, 최대값 최소값을 구해야 하니, 비교해야 할 ‘이전 상태’가 당연히 존재해야 함) ‘상태있는 연산’이라고 한다.
- IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
- 컬렉션뿐만 아니라 값,배열,파일,iterate, generate같은 메소드로도 스트림을 만들 수 있다.
- 무한한 개수의 요소를 가진 스트림을 무한 스트림이라고 한다.