스트림으로 데이터 수집
요약
Collect(Collectors.toList())
Collect(Collectors.counting())
Collect(Collectors.summarizingInt())
Collect(Collectors.joining(","))
Collect(Collectors.reducing(0, ~::getValue, Integer::Sum))
Collect(Collectors.groupingBy(Transaction::getCurrency))
- 스트림이 비었을 때를 대비해 Optional이 반환되곤 하는데, .orElse()등을 사용하여, 간접적으로 초깃값을 제공하는 것이 바람직하다.
연산 종류
중간 연산
- 한 스트림을 다른 스트림으로 변환하는 연산
- 여러 연산 연결 가능
- 스트림 파이프라인 구성
최종 연산
- 스트림의 요소를 소비해서 최종 결과 도출
- 재사용 불가
groupingBy
List<Transaction> transactions;
transactions.stream().collect(groupingBy(Transaction::getCurrency));
- 각 트랜잭션의 Currency 별로 그룹핑된 map이 반환됨
- 각 키, 그리고 각 키 벜닛에 대응하는 요소 리스트를 값으로 포함하는 Map을 만들라 !
기타
- Collect 메서드로 Collector 인터페이스 구현을 전달
- groupingBy도 Collector 인터페이스 구현 중 하나. Collectors.toList() 얘네들도 다 Collector 인터페이스 구현
- Collectors 유틸리티 클래스는 자주 사용하는 컬렉터 인스턴스를 손쉽게 생성할 수 있는 정적팩토리메서드를 제공 as (Collectors.toList())
Collector 인터페이스 구현체
summarization 연산
숫자 필드의 합계/평균 등 반환하는 연산
counting()
stream().collect(Collectors.counting())
stream().count()
- 상기 두개는 동일한 결과를 도출
maxBy()
- comparator 구현체를 maxBy의 parameter로 넘겨서 최대값 반환
summingInt()
stream().collect(summingInt(Dish::getCalories));
summarizingInt()
stream().collect(summarizingInt(Dish::getCalories));
- count, sum, min, average, max -> 모든 summarization 연산의 결과가
IntSummaryStatistics
클래스로 수집된다
문자열 연결
joining()
- 각 객체에 toString() 메서드를 호출해서, 호출한 모든 문자열을 하나의 문자열로 연결하여 반환
- 내부적으로 StringBuilder를 이용하여 문자열을 하나도 만듦
- cf) StringBuilder/String/StringBuffer의 차이 -> 부록에서 확인 가능
.collect(joining(", "))
범용 리듀싱
Collectors.reducing
- 사실 위에서 언급한 연산들 모두 reducing을 사용해서 정의가 가능하다.
- summarization 연산에서 사용하던 sum은 reducing을 이용하면 이런식으로 구현 가능
reducing(0, Dish::getCalories, (i,j) -> i+j))
- .collect(Collectors.reducing())은 사실 .reduce()와 닮아있다. 생각해보니 그러면 reduce로도 모든 Collector 구현체를 구현할 수 있는것 아닌가 ?
- 그러면 이제, reducing으로 하는게 나은지, 상기 메소드들을 사용하는게 좋은지 의문이 들겠지? 두개는 사실 설계 목적부터 달랐다고 함
- collect 메서드는 도출하려는 결과를 누적하는 컨테이너를 바꾸도록 설계된 메서드
- reduce는 두 값을 하나로 도출하는 불변형 연산
- reduce 메서드는 thread-unsafe로, 여러 스레드가 동시에 같은 데이터 구초레를 고치면 리듀싱 연산 병렬 수행이 불가능
- 따라서 병렬성 확보를 위해서는 collect 메서드로 리듀싱 연산을 구현하는 것이 바람직
그룹화
- groupingBy
- 보통 메서드 참조를 분류함수로 사용하나, 복잡해서 어려울 경우에는 람다 표현식으로 사용하면 된다.
- 지금까지 개발하면서는, 얘 파라미터가 하나인줄만 알았겠지만 사실 여러 파라미터를 받을 수 있다.
menu.stream()
.filter(dish -> dish.getCalories()>500)
.collect(groupingBy(Dish::getType))
- 얘가 우리가 일반적으로 아는 파라미터하나인 groupingBy
- 한계가 있다면, 필터 프리디케이트를 만족하지 못하는 요소는 아예 결과 맵에서 키 자체가 없어진다는 점
- 하지만 이렇게 변경하면, 키는 유지하고 내부 value만 없애는 식으로 변경할 수 있다.
menu.stream()
.collect(
groupingBy(Dish::getType
,filtering(dish -> dish.getCalories() > 500
, toList())
)
)
- groupingBy 내에서 두번째 파라미터로 mapping()을 넘길 수도 있고, 유사하게 flatMapping()도 사용할 수 있다. (coungting()도 사용 가능)
- groupingBy를 다중으로 사용하여 n-수준 groupingBy -> 결과는 n-수준 Map으로 배핑 가능하다.
- groupingBy의 두번째 파라미터에 또 groupingBy를 넘기는 방식 !
분할
Collector 인터페이스 직접 구현하기
- 상기 커스텀하게 제공되는 걸로 충분하지 않으면 직접 구현해서 사용하면 되는데
이럴 상황이 있을지는 잘 모르겠어서 그냥 대충 봤음.. 왠만하면 스트림 기본 제공 기본 구현체로 다 되는데..
- 구성은 supplier(), accumulator(), finisher(), combiner(), characteristics()
- supplier -> 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터가 없는 함수
- accumulator -> 누적자와 n번째 요소를 함수에 적용하는 BiConsumer로 구성
- finisher -> 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출하는 함수. 딱히 별거 없으면 Function.identity() 하면 될듯
- combiner -> 두 결과 컨테이너 병합 / 얘는 스트림의 서로 다른 서브파트를 병렬로 처리할 때만 명시해주면 됨
- Characteristics -> 스트림의 리듀스 방식 (병렬 여부) 또는, 병렬이라면 어떤 최적화로 리듀스 할지 힌트 제공
부록
StringBuilder/String/StringBuffer의 차이
- String: immutable 객체 / thread-safe
- 메모리에 생성 후 값 변경 불가능
- heap의 StringPool에 저장되고, 계속 남아있다가 GC로 사라짐
- immutable의 특성으로, 수정이 용이하지 않고 메모리의 낭비 초래
- thread-safe이므로 병렬 및 동기화에 케어 안해도됨
- StringBuffer: mutable 객체 / thread-safe
- 메모리에 생성 후 값 변경 가능
- 마찬가지로 heap영역에 저장되나, StringPool로는 사용 X
- thread-safe이므로 병렬 및 동기화에 케어 안해도됨
- StringBuilder: mutable 객체 / thread-unsafe
- 메모리에 생성 후 값 변경 가능
- 마찬가지로 heap영역에 저장되나, StringPool로는 사용 X
- thread-unsafe이므로 병렬처리 시 동기화 코드 구현 필요
- 하지만 단일스레드 환경에서는 StringBuffer보다 성능 good (thread 신경쓰지 않도록 코드가 구성돼있기 때문)
귀하의 게시물은 정말 귀중한 게시물입니다. 공유해 주셔서 감사합니다. 저는 게임 매니아입니다. 게임을 좋아한다면 이 웹사이트가 도움이 될 것입니다 ( https://www.ssegold.com/ ). 나는 당신에게 행복한 삶을 기원합니다.