5장
1. 필터링
1.1. filter
스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다. 다음은 모든 채식 요리를 필터링해서 채식 메뉴를 만드는 코드이다.
List<Dish> vegetarianMenu =
menu.stream()
.filter(Dish::isVegetarian)
.collect(toList());

1.2. distinct
스트림은 고유 요소로 이루어진 스트림을 반환하는 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);

2. 스트림 슬라이싱
다음과 같은 정렬된 요리 목록이 있다고 가정하자.
List<Dish> specialMenu = Arrays.asList(
new Dish("seasonal 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));
다양한 방법으로 스트림의 요소를 선택하거나 스킵할 수 있다.
2.1. takeWhile, dropWhile
자바 9는 스트림의 요소를 효과적으로 선택할 수 있도록 takeWhile, dropWhile 두 가지 새로운 메서드를 지원한다. 320 칼로리 이하의 요리를 선택해야한다고 생각해보자. filter 연산을 사용하면 전체 스트림을 반복하면서 각 요소에 프레디케이트를 적용한다. 리스트가 이미 정렬되어 있는 경우 전체 스트림을 반복하는 건 비효율적이다.
List<Dish> slicedMenu =
specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(toList());
takeWhile을 활용하면 무한 스트림을 포함한 모든 스트림에 프레이케이트를 적용해 스트림을 슬라이스할 수 있다.
List<Dish> slicedMenu =
specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(toList());
dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레이케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버리고 그 지점에서 작업을 중단하고 남은 요소를 반환한다. dropWhile 또한 무한 스트림을 포함한 모든 스트림에 적용할 수 있다.
2.2. limit
limit은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환한다. 다음은 프레디케이트와 일치하는 처음 세 요소를 반환하는 코드이다.
List<Dish> dishes =
specialMenu.stream()
.filter(dish -> dish.getCalories() > 300)
.limit(3)
.collect(toList());
정렬되지 않은 스트림의 경우 limit의 결과도 정렬되지 않는다.

2.3. skip
skip은 처음 n개 요소를 제외한 스트림을 반환한다. n개 이하의 요소를 포함하는 스트림에 skip을 호출하면 빈 스트림이 반환된다.
List<Dish> dishes =
menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(toList());

3. 매핑
매핑은 특정 객체에서 특정 데이터를 선택하는 작업을 말한다. 스트림 API는 map과 flatMap 메서드로 매핑을 지원한다.
3.1. map
map은 함수를 인수로 받아 각 요소에 적용하며 함수를 적용한 결과를 새로운 요소로 매핑한다.
List<String> dishNames =
menu.stream()
.map(Dish::getName)
.collect(toList());
만일 요리명의 길이를 알고 싶다면 다른 map 메서드를 연결(chaining)할 수 있다.
List<Integer> dishNames =
menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());
3.2. flatMap
["Hello", "World"]리스트가 있을 때 결과로 ["H", "e", "l", "o", "W", "r", "d"]라는 리스트를 반환해야 할 때 map을 사용하면 어떻게 될지 생각해보자.
words.stream()
.map(word -> word.split(""))
.distinct()
.collect(toList());
위 코드는 [["H", "e", "l", "l", "o"], ["W", "o", "r", "l", "d"]]를 반환한다. 우선 첫 번째 문제는 배열 스트림이다. 다음은 map이 반환하는 배열 스트림을 문자열 스트림으로 변경하는 코드이다.
words.stream()
.map(word -> word.split(""))
.map(Arrays::stream) // 각 배열을 별도 스트림으로 생성
.distinct()
.collect(toList());
여전히 결과값은 바뀌지 않았다. 문자열 스트림의 문자를 개별 스트림으로 만들어야 한다. flatMap은 스트림의 각 값을 다른 스트림으로 만든 다음 모든 스트림을 하나의 스트림으로 연결한다.
words.stream()
.map(word -> word.split(""))
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(toList());
4. 검색과 매칭
스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 특정 속성이 데이터 집합에 있는지 여부를 검색하는 다양한 메서드를 제공한다.
4.1. 검색
finalAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. 다음은 finalAny를 활용한 코드이다.
Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();
Optional<T> 클래스(java.util.Optional)은 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다. Optional은 값이 존재하는지 확인하고 값이 없을 때 어떻게 처리하는지 강제한다.
findFirst는 스트림에서 첫 번째 요소를 찾아 반환한다. 다음은 findFirst를 활용한 코드이다.
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
4.1. 매칭
anyMatch 메서드는 프레이케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 검사한다. 다음은 anyMatch를 활용하는 코드이다.
if (menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is perfect!");
}
allMatch 메서드는 프레디케이트가 주어진 스트림의 모든 요소와 일치하는지 검사한다. 다음은 allMatch를 활용하는 코드이다.
boolean isHealthy =
menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
noneMatch 메서드는 allMatch와 반대 연산을 수행한다. noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 검사한다. 다음은 noneMatch를 활용하는 코드이다.
boolean isHealthy =
menu.stream()
.noneMatch(dish -> dish.getCalories() >= 1000);
allMatch, anyMatch, noneMatch는 스트림 쇼트서킷 기법을 활용한다. 쇼트서킷은 전체 연산을 처리하지 않고 결과를 반환하는 걸 의미한다.
5. 리듀싱
리듀스(reduce) 연산은 스트림의 모든 요소를 반복적으로 처리한다. 함수형 프로그래밍에서는 폴드(fold)라고 부르기도 한다.
5.1. 요소의 합
reduce는 두 개의 인수를 갖는다.
초깃값 0
두 요소를 조합해서 새로운 값을 만드는 BinaryOperator
다음은 reduce를 활용하는 코드이다.
// reduce 활용
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
// 메서드 참조와 reduce 활용
int sum = numbers.stream().reduce(0, Integer::sum);
reduce에 초기값을 받지 않도록 오버로드하면 Optional 객체를 반환한다. 초기값이 없으면 합계를 반환할 수 없다. 따라서 합계가 없음을 가리키도록 Optional 객체로 감싼 결과를 반환하는 것이다.
5.2. 최댓값과 최솟값
최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다. 다음은 최댓값과 최솟값을 찾는 코드이다.
// 최댓값
Optional<Integer> max = numbers.stream().reduce(Integer::max);
// 최솟값
Optional<Integer> min = numbers.stream().reduce(Integer::min);
6. 숫자형 스트림
// 박싱 비용 발생
int calories =
menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
// sum 메서드 직접 호출(X)
int calories =
menu.stream()
.map(Dish::getCalories)
.sum();
reduce로 합계를 계산할 때 Integer를 기본형으로 언박싱하는 박싱 비용이 발생한다. 박싱 비용을 피하기 위해 sum 메서드를 직접 호출하는 건 불가능하다. map 메서드가 Stream를 생성하기 때문이다. 스트림은 박싱 비용을 피할 수 있도록 기본형 특화 스트림을 제공한다.
6.1. 기본형 특화 스트림
스트림 API는 숫자 스트림을 효율적으로 처리할 수 있는 세 가지 기본형 특화 스트림(primitive stream specialization)을 제공한다.
IntStream: int 요소 특화
DoubleStream: double 요소 특화
LongStream: long 요소 특화
각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다. 또한 다시 객체 스트림으로 복원하는 기능도 제공한다. 스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 사용한다. 다음은 숫자 스트림으로 매핑하는 코드이다.
int calories =
menu.stream()
.mapToInt(Dish::getCalories)
.sum();
특화 스트림이 아닌 다른 값으로 반환하고 싶을 때는 복원해야 한다. 다음은 특화 스트림을 객체 스트림으로 복원하는 코드이다.
// 스트림을 숫자 스트림으로 변환
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
// 숫자 스트림을 스트림으로 변환
Stream<Integer> stream = intStream.boxed();
값이 존재하는지 여부를 가리킬 때는 컨테이너 클래스 Optional을 사용한다. Optional은 OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전을 제공한다. 다음은 OptionalInt를 활용하는 코드이다.
OptionalInt maxCalories =
menu.stream()
.mapToInt(Dish::getCalories)
.max();
int max = maxCalories.orElse(1); // 값이 없을 때 기본 최댓값을 명시적으로 설정
6.2. 숫자 범위
자바 8의 IntStream과 LongStream에서 제공하는 range와 rangeClosed 메서드를 활용하면 특정 범위의 숫자를 활용할 수 있다. range는 시작값과 종료값이 결과에 포함되지 않는다. rancgeClosed는 시작값과 종료값이 결과에 포함된다.
IntStream evenNumbers =
IntStream.rangeClosed(1, 100)
.filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());
7. 스트림 만들기
7.1. Stream.of
임의의 수를 인수로 받는 정적 메서드 Stream.of를 활용해서 값으로 스트림을 만들 수 있다. 다음은 Stream.of로 문자열 스트림을 만드는 코드이다.
// Stream.of 활용
Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
stream.map(String::toUpperCase)
.forEach(System.out::println);
// 스트림 비우기
Stream<String> emptyStream = Stream.empty();
7.2. Stream.ofNullable
자바 9에 추가된 Stream.ofNullable을 활용해서 null이 될 수 있는 개체를 스트림을 만들 수 있다. 다음은 null 객체를 스트림으로 만드는 코드이다.
// 기본
String homeValue = System.getProperty("home");
Stream<String> homeValueStream =
homeValue == null ? Stream.empty() : Stream.of(value);
// Stream.ofNullable 활용
Stream<String> homeValueStream =
Stream.ofNullable(System.getProperty("home"));
// Stream.ofNullable을 flatMap과 함께 활용
Stream<String> values =
Stream.of("config", "home", "user")
.flatMap(key -> Stream.ofNullable(System.getProperty(key)));
7.3. Array.stream
배열을 인수로 받는 정적 메서드 Array.stream을 활용해서 스트림을 만들 수 있다. 다음은 배열을 스트림으로 만드는 코드이다.
int[] numbers = {1, 2, 3, 4, 5, 6};
int sum = Arrays.stream(numbers).sum();
7.4. Files.lines
I/O 연산에 사용하는 NIO API도 스트림 API를 활용할 수 있다. java.nio.file.Files의 많은 정적 메서드가 스트림을 반환한다. 다음은 Files.lines로 주어진 파일의 행 스트림을 문자열로 반환하는 코드이다.
long uniqueWords = 0;
try(Stream<String> lines =
Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
} catch(IOException e) { /* ... */ }
7.5. Stream.iterate, Stream.generate
무한 스트림(infinite stream)은 크기가 고정되지 않은 스트림을 말한다. Stream.iterate와 Stream.generate를 활용해서 요청할 때마다 함수로 스트림을 만들 수 있다. 다음은 iterate로 스트림을 만드는 코드이다.
// iterate 활용
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);
// filter와 iterate 함께 활용(X)
IntStream.iterate(0, n -> n + 4)
.filter(n -> n < 100)
.forEach(System.out::println);
// takeWhile과 iterate 함께 활용(O)
IntStream.iterate(0, n -> n + 4)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
iterate는 요청할 때마다 무한 스트림을 만드는데 이러한 스트림을 언바운드 스트림(Unbounded stream)이라고 표현한다. generate도 요구할 때 값을 계산하는 무한 스트림을 만들 수 있다. 하지만 generate는 생산된 각 값을 연속적으로 계산하지 않는다. generate는 Supplier를 인수로 받아 새로운 값을 생산한다. 다음은 generate로 스트림을 만드는 코드이다.
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
무한 스트림의 요소는 무한적으로 계산이 반복되므로 정렬하거나 reduce할 수 없다.
참고 자료
모던 자바 인 액션 - 한빛미디어
Last updated