stream 과 parallel stream 차이 (Java 8)

- A parallel stream has a much higher overhead to a sequential one. 
https://stackoverflow.com/questions/20375176/should-i-always-use-a-parallel-stream-when-possible?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

 

Should I always use a parallel stream when possible?

With Java 8 and lambdas it's easy to iterate over collections as streams, and just as easy to use a parallel stream. Two examples from the docs, the second one using parallelStream: myShapesCollec...

stackoverflow.com

위 내용을 참고해보면 stream을 쓰는것보다 parallel stream을 사용하는것이 오버헤드가 더 큰것을 알 수 있다. 즉, 병렬처리가 더 적합한 경우에 사용해야 한다. 일반적으로 parallelStream, parallel()을 사용할 경우 ForkJoinFramework(common fork join pool 사용)을 이용해서 작업을 분할하고 병렬적으로 처리하게 된다. 

reduce

스트림 요소를 조합해서 복잡한 질의를 만들 수 있는데 스트림의 모든 요소를 반복적으로 처리하게 된다. 이러한 질의를 리듀싱 연산(모든 스트림 요소를 처리해서 값으로 도출) 이라고 한다. 또, 특징적인것은 collect는 reduce를 wrapping한것이다. 이러한 자바 8부터 제공하는 Stream이라는 추상화를 사용하여 대용량 데이터를 메모리에 로드하지 않고 작업을 수행할 수 있도록 map - reduce 연산을 제공한다. map은 요소들을 특정조건에 해당하는 값으로 변환시켜 주고 reduce는 스트림의 항목을 컬렉션으로 재구성할 수 있다. 조금 더 일반적으로 얘기해보면 컬렉터로 스트림의 모든 항목을 하나의 결과로 합칠 수 있다는 의미와 같다. 

https://www.java67.com/2016/09/map-reduce-example-java8.html

collect

리듀싱 연산은 Collector 인터페이스의 구현으로 사용된다. Stream.collect 메서드의 인수로 스트림의 항목을 컬렉션으로 재구성할 수 있다. 

counting() 팩토리 메서드 : 개수를 반환한다. 

maxBy, minBy : 스트림의 최댓값과 최솟값을 계산할 수 있다. 

summingInt : 초깃값 0으로 설정되어 누적자에 값을 더한다. averagingLong, averagingDouble 다양한 형식으로 숫자 집합을 계산할 수 있다. 반환된 타입은 IntSummaryStatistics, LongSummaryStatistics, DoubleSummaryStatistics 클래스로 관련 정보가 수집된다. 

joining() : 추출한 모든 문자열을 하나의 문자열로 연결해서 반환한다. 

reducing 팩토리 메서드 : 첫 번째 인수는 연산의 시작값으로 시작한다. 두 번째 인수는 변환함수를 작성한다. 세 번째 인수는 같은 종류의 두 항목을 하나의 값으로 더하는 BinaryOperator로 동작한다. 

Optional<Dish> CalorieDish =
	menu.stream().collect(reducing(0, Dish::getCalories, (i,j) -> i+j));

 

collect vs reduce

collect 메서드는 도출하는 결과를 누적하는 값으로 바꾸지만, reduce 메서드는 두 값을 하나로 도출하는 불변형 연산을 수행한다. 그러므로 누적자가 필요한 연산에 대해서는 collect를 사용하는것이 좋다. 또, reduce를 잘못 사용하게 되면 병렬로 수행할 때 리스트가 망가지게 되는 경우가 발생하게 된다. 이 문제를 해결하기 위해서는 리스트를 매번 새롭게 할당해야 한다. 연산의 비용이 발생하기 때문에 전체적으로 성능 저하도 야기될 수 있다. 따라서, 병렬성을 확보하기 위해서는 collect 메서드로 리듀싱 연산을 구현하는것이 좋다. 아래 처럼 사용해야 리스트가 망가지지 않는다.

Stream<Integer> stream = Arrays.asList(1, 2, 3, 4, 5, 6)
                                       .stream();
        List<Integer> numbers = stream.parallel().reduce(
            new ArrayList<Integer>(),
            (List<Integer> l, Integer e) -> {
                ArrayList<Integer> integers = new ArrayList<>(l);
                integers.add(e);
                return integers;
            },
            (List<Integer> l1, List<Integer> l2) -> {
                ArrayList<Integer> integers = new ArrayList<>(l1);
                integers.addAll(l2);
                return integers;
            });

        numbers.forEach(number -> System.out.println(number));

Output : 1, 2, 3, 4, 5, 6