Post

2번 읽는 Modern Java In Action - Chapter05 스트림활용(2)

🔅 개요

저번 내용이 너무 길어질 것 같아서 따로 작성하게 되었다.

🔅 리듀싱

리듀싱 연산은 모든 스트림 요소를 처리해서 값으로 도출하는 것이라고 한다.

예시를 보는게 빠른 것 같다.

다음은 모든 요소에 대해 값을 더해서 그 결과를 반환하는 테스트이다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("reduce 튜토리얼")
void testReduce(){
        List<Integer> numbers = Arrays.asList(1,2,3,4,5);

        Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
        //간단버전
        //Integer sum = numbers.stream().reduce(0,Integer::sum);

        //1 + 2 + 3 + 4 + 5
        System.out.println(sum);
}

이처럼 reduce는 2 가지 인수를 가진다.

  • 초기값
  • 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T> 이다.

reduce에는 초기값 없는 경우도 오버로드 되어있다.

1
2
3
4
//초기값 없는 경우
Optional<Integer> result = numbers.stream().reduce(Integer::sum);
//Optional[15]
System.out.println(result);

스트림에 아무 요소가 없는 경우가 있을 수 있으므로 Optional을 반환하게 된다.

최댓값 최솟값

reduce를 사용하면 최댓값, 최솟값을 찾을 수 있다.

인자로는 위에서 말한 두 요소를 조합해서 새로운 값을 만드는 BinaryOperator<T>형태의 값을 넣으면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
@Test
@DisplayName("reduce max min")
void testReduceMaxMin(){
    Optional<Integer> max = numbers.stream().reduce(Integer::max);
    Assertions.assertEquals(max.get(), 5);

    Optional<Integer> min = numbers.stream().reduce(Integer::min);
    Assertions.assertEquals(min.get(), 1);

    Optional<Integer> min2 = numbers.stream().reduce((x, y) -> x < y ? x : y);
    Assertions.assertEquals(min2.get(), 1);
}

이런식으로 넘기면 된다.

🔅 숫자형 스트림

1
2
3
int calories = menu.stream()
    .map(Dish::getCalories)
    .reduce(0,Integer::sum);

이런 코드가 있는데, 위에는 박싱 비용이 숨겨져 있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형(int)으로 언박싱해야 한다.

그리고, reduce연산 말고 sum()하나로 합계를 구할 수 있다면 더 편할 것 같다.

1
2
3
int calories = menu.stream()
    .map(Dish::getCalories)
    .sum()

하지만 이는 불가능하다. map()의 반환형이 Stream<T>이기 때문에 해당 인터페이스가 sum을 가질 수 없기 때문이다.

이는 비효율적인데, 스트림 API 숫자 스트림을 효율적으로 처리할 수 있도록 기본형 특화 스트림을 제공한다.

기본형 특화 스트림

자바8에서는 스트림 API는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream을 제공한다.

위의 스트림들은 각각 숫자 스트림의 합계를 계산하는 sum, 최댓값을 구하는 max와 같은 연산 수행 메서드도 제공한다.

1
2
3
4
5
6
7
8
@Test
@DisplayName("숫자 스트림 매핑")
void testMapToInt(){
    int calories = menu.stream() // Stream<Dish>
            .mapToInt(Dish::getCalories) // IntStream으로 반환
            .sum();
    System.out.println(calories);
}

위의 mapToIntIntStream을 반환한다. 이 인터페이스가 제공하는 sum을 통하여 합계를 계산할 수 있게 되었다.

참고로 스트림이 비어있다면 sum은 기본값 0을 반환한다. 외에도 max, min, average 등 다양한 유틸리티 메서드도 지원한다.

객체 스트림으로 복원

IntStream을 그냥 Stream을 바꿀 수 있을까에 대한 내용이다.

간단하게 boxed라는 메서드를 사용하면 된다.

1
2
3
4
5
6
7
@Test
@DisplayName("boxed 메서드")
void testBoxed(){
    IntStream intStream = menu.stream()
            .mapToInt(Dish::getCalories);
    Stream<Integer> boxed = intStream.boxed();
}

기본값들

기본형 특화 스트림은 max메서드도 제공하는데, 스트림이 비어있는 경우 이를 어떻게 판단할까?

IntStream기준으로 이를 담기 위한 OptionalInt라는 자료형을 제공한다.

1
2
3
4
5
6
7
8
9
@Test
@DisplayName("OptionalInt")
void testOptionalInt(){
    OptionalInt maxCalories = menu.stream()
            .mapToInt(Dish::getCalories)
            .max();

    System.out.println(maxCalories.orElse(1));
}

OptionalInt자료형을 통해 최댓값이 없는 상황에 사용할 기본값을 명시적으로 정의가 가능하다.

사실 왜 Optional을 사용하지 않았을까 생각하였는데, 가독성면에서 좀 더 특화된 OptionalInt를 사용하는게 낫겠다 생각하였다.

숫자 범위

특정 범위의 숫자를 이용해야 하는 상황이 있는데, 이를 지원한다.

rangerangeClosed라는 두 가지 정적 메서드가 있는데, 첫 번째 인수로는 시작값을 두 번째 인수로는 종료값을 갖는다.

range는 시작값과 종료값이 결과에 포함되지 않고, rangeClosed는 포함된다.

1
2
3
4
5
6
7
8
9
10
@Test
@DisplayName("숫자범위")
void testRange() {
    IntStream evenNumbers = IntStream.rangeClosed(1, 100)
            .filter(n -> n % 2 == 0);
    IntStream evenNumbers2 = IntStream.range(1, 100)
            .filter(n -> n % 2 == 0);
    Assertions.assertEquals(evenNumbers.count(), 50);
    Assertions.assertEquals(evenNumbers2.count(), 49);
}

🔅 스트림 만들기

다양한 방식으로 스트림을 만들 수 있다고 한다.

1
2
3
4
5
6
7
8
//값으로 스트림 만들기
@Test
@DisplayName("값으로 스트림 만들기")
void testStreamByValue(){
    Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");
    stream.map(String::toUpperCase)
            .forEach(System.out::println);
}
1
2
3
//널값을 가지는 스트림 만들기
Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

자고로 System.getProperty()는 key값으로 들어온 인자에 해당하는 값이 없으면 null을 반환한다. 이를 넣을 수 있다. Java 9부터다.

1
2
3
4
5
6
7
8
//배열로 스트림 만들기
@Test
@DisplayName("배열로 스트림 만들기")
void testArrayStream(){
    int[] numbers = {2,3,5,7,11,13};
    int sum = Arrays.stream(numbers).sum();
    System.out.println("sum : " + sum);
}
1
2
3
4
5
6
7
8
9
10
//무한스트림 만들기 iterate활용
@Test
@DisplayName("무한 스트림")
void testInfiniteStream(){
    //iterate활용
    Stream.iterate(0, n -> n +2)
            .limit(10)
            .forEach(System.out::println);
    //0 2 4 6 8 10 ...18
}

iterate()메서드는 첫 번째 인자로는 초기값을 가지고, 두 번째 인자로는 람다(UnaryOperator<T>)를 가져 새로운 값을 끊임없이 생산한다고 한다. 이러한 스트림을 무한 스트림이라하며 동일하게 언바운드 스트림이라고 한다.

보통 연속된 일련 값을 만들때는 iterate()를 사용한다고 한다.

1
2
3
4
5
6
7
8
9
//무한 스트림 만들기 generate 활용
@Test
@DisplayName("generate활용")
void testGenerateStream(){
    //generate활용
    Stream.generate(Math::random)
            .limit(5)
            .forEach(System.out::println);
}

generate()는 앞에서 설명한 iterate()와 다르게 계산을 하지 않는다.

이 두 메서드의 차이점은 책예제로는 피보나치를 구현하면 알 수 있다고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//iterate로 피보나치 구현
Stream.iterate(new int[]{0,1},t -> new int[]{t[1], t[0] + t[1]} )
        .limit(20)
        .forEach(t -> System.out.println("(" + t[0] + "," + t[1] + ")"));

//generate로 구현
IntSupplier fib = new IntSupplier() {
    private int previous = 0;
    private int current = 1;

    @Override
    public int getAsInt() {
        int oldPrevious = this.previous;
        int nextValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};
IntStream.generate(fib)
        .limit(10)
        .forEach(System.out::println);

generate의 인자는 상태를 가질 수 있다.

위 코드를 보면 IntSupplier의 상태를 갱신하고 있다. 즉, getAsInt()를 호출하면서 자신의 상태를 바꾸며 값을 생산하고 있다. 반면 iterate는 값은 새로 생성하지만 기존 상태를 바꾸지 않으려는 불변 상태를 유지하려고 한다.

근데 병렬처리에 있어서는 불변함을 고수해야한다는 것을 강조하는데, generategetAsInt()를 람다로 표현하지 않으면 개발자가 커스텀하여, 불변함을 유지할 수 없다. 때문에 사용하는데 주의가 더 필요한 것은 사실인 것 같다.

This post is licensed under CC BY 4.0 by the author.