/////
Search
Duplicate
7️⃣

숫자형 스트림

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은 기본형의 정수값만 만들 수 있다. IntStreammap 연산은 ‘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을 언급한 적이 있다.
OptionalInteger, 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의 IntStreamLongStream에서는 rangerangeClosed라는 두 가지 정적 메서드를 제공한다.
두 메서드 모두 첫 번째 인수로 시작값을, 두 번째 인수로 종료값을 갖는다.
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가 반환한 IntStreamboxed로 이용해서 Stream<Integer>로 복원했다.
map은 스트림의 각 요소를 int 배열로 변환하기 때문이다.
IntStreammap 메서드는 스트림의 각 요소로 int가 반환될 것을 기대하지만 이는 우리가 원하는 연산이 아니다.
객체값 스트림을 반환하는 IntStreammapToObj를 이용해서 이 코드를 재구현하자.
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
복사