•
5.4절에서 reduce 메서드로 스트림 요소의 합을 계산하는 예제를 살펴봤다.
◦
다음처럼 메뉴의 칼로리 합계를 계산할 수 있게 될 것이다.
int calories = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
Java
복사
◦
사실 위 코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야 한다.
◦
만약 다음처럼 직접 sum 메서드를 호출할 수 있다면 더 좋지 않을까?
int calories = menu.stream()
.map(Dish::getCalories)
.sum();
Java
복사
◦
하지만 위 코드처럼 sum 메서드를 직접 호출할 수 없다. map 메서드가 Stream<T>를 생성하기 때문이다.
◦
스트림의 요소 형식은 Integer지만 인터페이스에는 sum 메서드가 존재하지 않는다.
◦
왜 sum 메서드가 없을까? 예를 들어 menu처럼 Stream<Dish> 형식의 요소같은 경우라면 sum이라는 연산을 수행할 수 없기 때문이다.
◦
다행히도 스트림 API은 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.
1. 기본형 특화 스트림
•
자바 8에서는 세 가지 기본형 특화 스트림을 제공한다.
•
스트림 API는 박싱 비용을 피할 수 있도록 ‘int 요소에 특화된 IntStream’, ‘double 요소에 특화된 DoubleStream’, ‘long 요소에 특화된 LongStream’을 제공한다.
•
각각의 인터페이스는 숫자 스트림의 합계를 계산하는 sum, 최댓값 요소를 검색하는 max와 같이 자주 사용하는 숫자 관련 리듀싱 연산 수행 메서드를 제공한다.
•
필요할 때 다시 객체 스트림으로 복원하는 기능도 제공한다.
•
특화 스트림은 오직 박싱 과정에서 발생하는 효율성과 관련이 있으며 스트림에 추가 기능을 제공하지는 않는다.
숫자 스트림으로 매핑
•
스트림을 특화 스트림으로 변환할 때는 mapToInt, mapToDouble, mapToLong 세 가지 메서드를 가장 많이 사용한다.
•
이들 메서드는 정확히 map과 같은 기능을 수행하지만 Stream<T> 대신 특화된 스트림을 반환한다.
int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
Java
복사
•
mapToInt 메서드는 각 요리에서 모든 칼로리를 추출한 다음에 IntStream을 반환한다.
•
따라서 IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 합계를 계산할 수 있었다.
•
스트림이 비어있다면 sum은 기본값인 0을 반환한다.
객체 스트림으로 복원하기
•
숫자 스트림을 만든 다음에, 원상태인 특화되지 않은 스트림으로 복원할 수 있을까?
•
예를 들어 IntStream은 기본형의 정수값만 만들 수 있다. IntStream의 map 연산은 ‘int를 인수로 받아서 int를 반환하는 람다(IntUnaryOperator)’를 인수로 받는다.
•
정수가 아닌 Dish와 같은 다른 값을 반환하고 싶다면? 그러려면 스트림 인터페이스에 정의된 일반적인 연산을 사용해야 한다.
•
다음 예제처럼 boxed 메서드를 이용해서 특화 스트림을 일반 객체 스트림으로 변환할 수 있다.
IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();
Java
복사
기본값: OptionalInt
•
합계 에제에서는 0이라는 기본값이 있었으므로 nullable에 대해 별 문제가 없었다. 하지만 IntStream의 최댓값을 구하는 max 메서드를 사용할 경우, 기본값 0 때문에 문제가 발생할 수 있다.
•
스트림에 요소가 없어 0인 경우와 실제 최댓값이 0인 상황을 어떻게 구별할 수 있을까?
•
이전에 값이 존재하는지 여부를 가리킬 수 있는 컨테이너 클래스인 Otpional을 언급한 적이 있다.
•
Optional을 Integer, String 등의 참조 형식으로 파라미터화할 수 있다.
•
OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.
•
예를 들어 다음처럼 OptionalInt를 이용해서 IntStream의 최댓값 요소를 찾을 수 있다.
OptionalInt maxCalories = menu.stream()
.mapToInt(Dish::getCalories)
.max();
Java
복사
•
이제 OptionalInt를 이용해서 최댓값이 없는 상황에서 사용할 기본값을 명시적으로 정의해줄 수 있다.
int max = maxCalories.orElse(1); // 값이 없을 때 기본 최댓값을 명시적으로 설정
Java
복사
2. 숫자 범위
•
프로그램에서는 특정 범위의 숫자를 이용해야 하는 상황이 자주 발생한다.
•
예를 들어 1에서 100 사이의 숫자를 생성하려 한다고 가정하자.
•
자바 8의 IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.
•
두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.
•
range 메서드는 시작값과 종료값이 포함되지 않는 반면, rangeClosed는 시작값과 종료값이 결과에 포함된다는 점이 다르다.
•
다음 예제를 살펴보자.
IntStream evenNumbers = IntStream.rangeClosed(1, 100) // [1, 100]
.filter(n -> n % 2 == 0) // 짝수 스트림
.count()
Java
복사
•
위 코드처럼 rangeClosed를 이용해서 1부터 100까지의 숫자를 만들 수 있다. rangeClosed의 결과는 스트림으로 filter 메서드를 이용해서 짝수만 필터링할 수 있다.
•
filter를 호출해도 실제로는 아무 계산도 이루어지지 않는다. 최종적으로 결과 스트림에 count를 호출하는데, 최종 연산이므로 이때 스트림을 처리해서 1부터 100까지의 범위에서 짝수 50개를 반환한다.
•
이때 rangeClosed 대신에 IntStream.range(1, 100)을 사용하면 1과 100을 포함하지 않으므로 짝수 49개를 반환한다.
3. 숫자 스트림 활용 : 피타고라스 수
•
지금까지 배운 숫자 스트림과 스트림 연산을 더 활용할 수 있는 어려운 예제를 살펴보자. 우리의 임무는 ‘피타고라스 수’ 스트림을 만드는 것이다.
피타고라스 수
•
피타고라스 수는 a * a + b * b = c * c를 만족하는 세 개의 정수 (a,b,c)를 말한다.
세 수 표현하기
•
우선 세 수를 정의해야 한다. 세 수를 표현할 클래스를 정의하는 것보다는 세 요소를 갖는 int 배열을 사용하는 것이 좋을 것 같다.
•
예를 들어 (3, 4, 5)를 new int[]{3,4,5}로 표현할 수 있다. 이제 인덱스로 각 배열의 요소에 접근할 수 있다.
좋은 필터링 조합
•
누군가 세 수 중에서 a, b 두 수만 제공했다고 가정하자.
•
두 수가 피타고라스 수의 일부가 될 수 있는 좋은 조합인지 어떻게 확인할 수 있을까?
•
a * a + b * b의 제곱근이 정수인지 확인하면 된다. 자바 코드로는 다음과 같이 구현할 수 있다.
Math.sqrt(a * a + b * b) % 1 == 0
Java
복사
•
이를 filter에 다음처럼 활용할 수 있다.
filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
Java
복사
•
위 코드에서 a는 고정되고 b는 스트림으로 제공된다고 가정할 때 filter로 a와 함께 피타고라스 수를 생성하는 모든 b를 필터링할 수 있다.
집합 생성
•
필터를 이용해서 좋은 조합을 갖는 a, b를 선별할 수 있게 되었다. 이제 마지막 세 번째 수를 찾아야 한다.
•
다음처럼 map을 이용해서 각 요소를 피타고라스 수로 변환할 수 있다.
stream.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
.map(b -> new int[](a, b, (int) Math.sqrt(a * a + b * b)});
Java
복사
b값 생성
•
이제 b값을 생성해야 한다.
•
Stream.rangeClosed로 1부터 100까지의 b값을 만들어보자.
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
.boxed()
.map(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
Java
복사
◦
filter 연산 다음에 rangeClosed가 반환한 IntStream을 boxed로 이용해서 Stream<Integer>로 복원했다.
◦
map은 스트림의 각 요소를 int 배열로 변환하기 때문이다.
◦
IntStream의 map 메서드는 스트림의 각 요소로 int가 반환될 것을 기대하지만 이는 우리가 원하는 연산이 아니다.
◦
객체값 스트림을 반환하는 IntStream의 mapToObj를 이용해서 이 코드를 재구현하자.
IntStream.rangeClosed(1, 100)
.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)});
Java
복사
a값 생성
•
마지막으로 a값을 생성하는 코드를 추가한다. 그러면 피타고라스 수를 생성하는 스트림을 생성할 수 있다.
Stream<int[]> pyhangoreanTriples = IntStream.rangeClosed(1, 100).boxed()
.flatMap(a -> IntStream.rangeClosed(a, 100)
.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)
.mapToObj(b -> new int[]{a, b, (int) Math.sqrt(a * a + b * b)}));
Java
복사
•
여기서 flatMap은 어떤 연산을 수행하는 걸까? 우선 a에 사용할 숫자를 1부터 100까지 생성한다.
•
그리고 주어진 a를 이용해서 세 수의 스트림을 만든다. 스트림 a의 값을 매핑하면 스트림의 스트림이 만들어질 것이다.
•
따라서 flatMap 메서드는 생성된 각각의 스트림을 하나의 평준화된 스트림으로 만들어준다. 결과적으로 세 수로 이루어진 스트림을 얻을 수 있다.
•
또한 b의 범위가 a에서 100으로 변경된 점도 유의하자. b를 1부터 시작하면 중복된 세수가 생성될 수도 있다.
코드 실행
•
이제 코드 구현은 완료되었고 limit을 이용해서 얼마나 많은 세 수를 포함하는 스트림을 만들 것인지만 결정하면 된다.
pythagoreanTriples.limit(5)
.forEach(t -> System.out.println(t[0] + ", " + t[1] + ", " + t[2]));
Java
복사