빙응의 공부 블로그

[JAVA]함수형 프로그래밍 - Stream API 고급 본문

JAVA

[JAVA]함수형 프로그래밍 - Stream API 고급

빙응이 2023. 12. 2. 16:38

📝FlatMap을 통한 중첩 구조 제거

FlatMap은 스트림의 각 요소를 다른 스트림으로 매핑한 후에 이를 단일 스트림으로 평탄화하는 데 사용된다.

즉 데이터가 2중 배열 혹은 2중 리스트로 되어있는데 1차원으로 처리해야 하는 경우 사용한다. 

 

flatMap은 Function 함수형 인터페이스를 매개 변수로 받는다. 

        // 중첩된 배열
        Integer[][] nestedArray = {
            {1, 2, 3},
            {4, 5},
            {6, 7, 8}
        };

        // 배열을 스트림으로 평탄화하고 리스트로 변환
        List<Integer> flatList = Stream.of(nestedArray)
                .flatMap(Arrays::stream)
                .collect(Collectors.toList());

 

        // 중첩된 리스트
        List<List<Integer>> nestedList = Arrays.asList(
            Arrays.asList(1, 2, 3),
            Arrays.asList(4, 5),
            Arrays.asList(6, 7, 8)
        );

        // 리스트를 스트림으로 평탄화하고 리스트로 변환
        List<Integer> flatList = nestedList.stream()
                .flatMap(List::stream)
                .collect(Collectors.toList());

 

 

📝Reduce를 통한 결과 생성

Reduce는 누산기(누적 계산)와 연산으로 컬렉션에 있는 값을 처리하여 더 작은 컬렉션이나 단일 값을 만드는 작업이다.

누산기와 연산이기 때문에 기존 수학적 연산이 가능하다.

list.stream()
	.reduce(Integer::sum)
    .get();
 
list.stream()
	.reduce(0,(a,b)-> a+b)
    .get();

 

이처럼 Reduce 함수는 여러 요소들을 통해 새로운 결과를 만들어낸다.

받을 수 있는 매개변수는 3개이다.

<T> T reduce(T identity, BinaryOperator<T> accumulator, BinaryOperator<T> combiner)

  • identitiy : 초기값으로 사용되는 값이다.
  • BinaryOperator<T> accumulator : 스트림의 각 요소를 독립적으로 처리하여 부분 결과를 누적하는 역할
  • BinaryOperator<T> combiner : 병렬 스트림에서 동작할 때 사용되는 파라미터이다. 즉 실행 중에 서로 다른 서브 스트림의 결과를 합칠 때 사용한다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

// 초기값 0을 사용하여 리스트의 모든 요소를 더함 (병렬 스트림에서는 combiner가 사용됨)
int sumParallel = numbers.parallelStream().reduce(0, (a, b) -> a + b, Integer::sum);
병렬 스트림 Combiner의 이해 

사실상 accumulator과 combiner는 왜 사용할까?

그것은 병렬 처리에 있다.

가장 비슷한 예로는 알고리즘의 Divide-and-Conquer 방식이다. 

Divide-and-Conquer 방식은 문제를 분해해서 정렬한 뒤 합병하는 방식을 취하고 있다. Reduce도 비슷한 방식이다.

 

accumulator은 각 스레드가 독립적으로 작업을 수행하여 각각의 부분 결과를 누적한다. 

그리고 Combiner가 누적된 부분 결과들을 최종 결과로 합치는 역할을 한다. 

 

 

 

📝Null-Safe한 Stream 생성하기

Java를 이용해 개발을 하다보면 NPE(NullPointerException)이 매우 자주 발생한다. 

물론 NPE를 방지하기 위해 Null 여부 검사 코드를 작성할 수 있지만, 기본적으로 이러한 코드는 가독성을 해친다.

이러한 문제를 해결하기 위해 Java8부터 Optional이라는 Wrapper 클래스를 제공하여 Null을 처리할 수 있게 도와주고 있다.

Stream 또한 Optional과 궁합이 좋은 편이다.

 

[JAVA] Optional

📝 JAVA 8 Optional 자바 옵셔널은 자바 8에서 최초로 도입 되었다. 그 이유는 바로 NULL 때문이다. 프로그래밍을 하다보면 NULL 처리를 필수적으로 하게 된다. 기존에는 NULL 체크를 해서 NULL이 아닌 경

quddnd.tistory.com

public static <T> Stream<T> collectionToStream(Collection<T> collection) {
    return Optional
            .ofNullable(collection)
            .map(Collection::stream)
            .orElseGet(Stream::empty);
} //값이 들어오면 컬렉션스트림 생성, NULL이면 빈 스트림 리턴

위 코드처럼 만약 NULL 값이 들어오면 빈스트림을 만들어서 리턴한다.

 

스트림은 기본적으로 NULL을 허용하지 않기 때문에 Optional을 이용해 처음에 처리 해줘야 NPE에 안전하다.

빈 스트림은 요소가 없기 때문에 어떤 작업을 수행하더라도 아무런 영향이 없다.

 

 

📝Stream의 실행 순서 고려

Stream의 처리 순서는 무엇일까?

스트림의 특징은 병렬처리에 있다. 그렇기에 스트림은 각 요소를 개별적으로 처리하는 방법을 사용한다. 

아래의 예를 보자 

Stream.of("a", "b", "c", "d", "e")
        .filter(s -> {
            System.out.println("filter: " + s);
            return true;
        })
        .forEach(s -> System.out.println("forEach: " + s));
/*
filter: a
forEach: a
filter: b
forEach: b
filter: c
forEach: c
filter: d
forEach: d
filter: e
forEach: e
*/

 

우리가 평소에 보는 코드라면 절차지향 방법으로 모든 요소를 filter로 처리하고 그 반환 값을 forEach에 전달한다. 고 생각한다. 그러나 Stream은 각 요소마다 해당 스트림 연산을 순회한다. 

즉, a,b,c 요소가 있을 때 a의 연산이 끝나면 b로 넘어간다. 

 

📝Stream의 병렬 처리 

Stream은 아주 많은 양의 데이터를 처리해야 하는 경우 런타임 성능을 높이기 위해 병렬 스트림을 제공하고 있다. 

기본적으로 Fork-Join 즉 알고리즘의 Divide-and-conquer을 기반으로 하며 적절히 분할 후 결과를 합치는 방식을 사용한다.

        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        int sum = Arrays.stream(numbers)
                        .parallel() // 병렬 처리 활성화
                        .map(i -> i * 2)
                        .sum();

        System.out.println("Sum: " + sum);
        
        
        //Reduce도 병렬 처리이다
        int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

        // reduce를 사용하여 모든 요소의 합을 계산
        int sum = Arrays.stream(numbers)
                        .reduce(0, (acc, element) -> acc + element);

        System.out.println("Sum: " + sum);

 

물론 위 코드와 같이 작은 데이터의 경우에는 오버헤드로 인해 오히려 성능이 떨어질 수 있다. 

멀티코어 환경이나, 데이터가 큰 경우 사용하는 것을 권장한다. 

 

 

 

 


✔참조 사이트