Stream 의 마지막 단계에서 사용되는 Java Collector에 대해 살펴보겠습니다.
1. Stream.collect()
Stream.collect()는 데이터의 중간 처리 후 마지막에 원하는 형태로 변환해 주는 역할을 합니다. collector는 아래와 같은 기능들을 제공하며 이외에도 많은 기능들을 제공합니다.
- stream 요소들을 List, Set, Map 자료형으로 변환
- stream 요소들의 결합(joining)
- stream 요소들의 통계(최대, 최소, 평균값 등…)
- stream 요소들의 그룹화와 분할
2. Collectors
미리 정의된 모든 구현은 Collectors 클래스에서 찾을 수 있습니다. 가독성을 높이기 위해 다음과 같이 정적 import를 사용하는 것이 일반적입니다.
import static java.util.stream.Collectors.*;
또는 아래와 같이 사용할 Collector만 개별로 선언하여 사용하기도 합니다.
import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static java.util.stream.Collectors.toSet;
싥습 예제에서는 아래의 List를 가지고 실습을 진행합니다.
List<String> sampleList = Arrays.asList("a", "bb", "ccc", "dddd", "a");
2.1. Collectors.toList()
모든 Stream의 요소를 List 인스턴스로 수집하는 데 사용할 수 있습니다. 해당 메서드는 특정한 List를 구현하는 것이 아니며 이것을 좀 더 잘 제어하려면 toCollection을 대신 사용할 수 있습니다.
아래는 Stream의 모든 요소를 List 인스턴스로 수집하는 예제입니다.
List<String> result = sampleList .stream().collect(toList());
2.2. Collectors.toSet()
모든 Stream의 요소를 Set 인스턴스로 수집하는 데 사용할 수 있습니다. 해당 메서드는 특정한 Set를 구현하는 것이 아니며 이것을 좀 더 잘 제어하려면 toCollection을 대신 사용할 수 있습니다.
아래는 Stream의 모든 요소를 Set 인스턴스로 수집하는 예제입니다. set에는 중복 요소가 없어 서로 같은 요소가 포함된 경우 결과에 한 번만 나타납니다.
Set<String> result = sampleList.stream().collect(toSet()); assertThat(result).hasSize(4);
2.3. Collectors.toCollection()
위에서 설명했듯이 toList, toSet Collector는 특정한 List, Set의 구현을 지정할 수 없습니다. 특정 Collection을 구현하려면 toCollection collector를 사용하면 됩니다.
List<String> lists = sampleList.stream().collect(toCollection(LinkedList::new)) Set<String> sets = sampleList.stream().collect(toCollection(TreeSet::new))
변경 불가능한 컬렉션에서는 작동하지 않습니다. 이 경우 사용자 정의 수집기 구현을 작성하거나 collectAndThen을 사용해야 합니다.
2.4. Collectors.toMap()
toMap Collector는 stream요소를 Map 인스턴스로 수집하는 데 사용할 수 있습니다. 이렇게 하려면 두 가지 기능을 제공해야 합니다.
- keyMapper
- valueMapper
keyMapper는 Stream요소에서 Map의 Key를 추출하는 데 사용되며 valueMapper는 지정된 키와 관련된 값을 추출하는 사용 됩니다.
아래는 문자열을 key로 저장하고, 길이를 value로 하여 Map에 요소를 수집하는 예제입니다.
Map<String, Integer> result = sampleList.stream().collect(toMap(Function.identity(), String::length))
Function.identity() 는 stream에 전달된 요소의 값을 그대로 반환합니다.
만약 collection에 중복 요소가 포함되어 있으면 toSet과 달리 toMap은 중복을 자동으로 필터링하지 않습니다. 따라서 중복 키가 보이면 즉시 IllegalStateException이 발생합니다.
List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb"); assertThatThrownBy(() -> { listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));}).isInstanceOf(IllegalStateException.class);
key 충돌이 있는 경우 다른 signature와 함께 toMap을 사용해야 합니다.
Map<String, Integer> result = sampleList.stream().collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
여기서 세 번째 인자는 BinaryOperator로, 충돌 처리 방법을 지정할 수 있습니다. 이 경우 동일한 문자열의 길이가 항상 동일하다는 것을 알기 때문에 두 충돌 값 중 하나만 선택합니다.
2.5. Collectors.collectingAndThen()
CollectingAndThen은 수집이 끝난 직후 결과에 대해 다른 작업을 수행할 수 있는 특수한 collector입니다.
아래 예제는 stream요소를 List인스턴스로 수집 한 다음 결과를 ImmutableList 인스턴스로 변환합니다.
List<String> result = sampleList.stream().collect(collectingAndThen(toList(), ImmutableList::copyOf));
2.6. Collectors.joining()
Joining collector는 Stream<String>의 요소를 결합하는데 사용할 수 있습니다.
String result = sampleList.stream().collect(Collectors.joining()); assertThat(result).isEqualTo("abbcccdddda");
사용자 정의 구분 기호, 접두사, 접미사를 지정할 수도 있습니다.
List<String> sampleList = Arrays.asList("a", "bb", "ccc", "dddd", "a"); String result = sampleList.stream().collect(Collectors.joining(" ")); assertThat(result).isEqualTo("a bb ccc dddd a");
다음과 같이 사용할 수 있습니다.
String result = sampleList.stream().collect(Collectors.joining(" ","PRE-", "-POST")); assertThat(result).isEqualTo("PRE-a bb ccc dddd a-POST");
2.7. Collectors.counting()
Counting은 모든 스트림 요소를 간단히 카운팅 할 수있는 Collector입니다.
Long result = sampleList.stream().collect(counting()); assertThat(result).isEqualTo(5);
간략하게 다음과 같이 작성도 할수 있습니다.
Long result = sampleList.stream().count();
2.8. Collectors.summarizingDouble/Long/Int()
SummarizingDouble/Long/Int는 추출된 요소 stream에서 숫자 데이터에 대한 통계 정보를 포함하는 특수 클래스를 리턴하는 collector입니다.
DoubleSummaryStatistics result = sampleList.stream().collect(summarizingDouble(String::length)); assertThat(result.getAverage()).isEqualTo(2.2); assertThat(result.getCount()).isEqualTo(5); assertThat(result.getMax()).isEqualTo(4); assertThat(result.getMin()).isEqualTo(1); assertThat(result.getSum()).isEqualTo(11);
2.9. Collectors.averagingDouble/Long/Int()
AveragingDouble/Long/Int는 추출된 요소의 평균을 반환하는 collector입니다.
Double result = sampleList.stream().collect(averagingDouble(String::length)); assertThat(result).isEqualTo(2.2);
2.10. Collectors.summingDouble/Long/Int()
SummingDouble/Long/Int는 추출 된 요소의 합계를 반환하는 collector입니다.
Double result = sampleList.stream().collect(summingDouble(String::length)); assertThat(result).isEqualTo(11);
2.11. Collectors.maxBy()/minBy()
MaxBy/MinBy collector는 제공된 Comparator 인스턴스에 따라 Stream의 가장 큰 / 가장 작은 요소를 반환합니다.
Optional<String> result = sampleList.stream().collect(maxBy(Comparator.naturalOrder())); assertThat(result).hasValue("dddd");
Optional<String> result = sampleList.stream().collect(minBy(Comparator.naturalOrder())); assertThat(result).hasValue("a");
2.12. Collectors.groupingBy()
GroupingBy collector는 일부 속성별로 객체를 그룹화하고 결과를 Map 인스턴스에 저장하는 데 사용됩니다.
문자열 길이로 그룹화하고 그룹화 결과를 Set 인스턴스에 저장하는 예제입니다.
Map<Integer, Set<String>> result = sampleList.stream().collect(groupingBy(String::length, toSet())); // result => {1=[a], 2=[bb], 3=[ccc], 4=[dddd]}
groupingBy 메소드의 두 번째 인수는 Collector이며 원하는 Collector를 자유롭게 사용할 수 있습니다.
2.13. Collectors.partitioningBy()
PartitioningBy는 Predicate 인스턴스를 허용하고 스트림 요소를 Boolean 값을 키로, 컬렉션을 값으로 저장하는 Map 인스턴스로 수집합니다. “true”키 아래에서 주어진 술어와 일치하는 요소 컬렉션을 찾을 수 있으며 “false”키 아래에서 주어진 술어와 일치하지 않는 요소 컬렉션을 찾을 수 있습니다.
Map<Boolean, List<String>> result = sampleList.stream().collect(partitioningBy(s -> s.length() > 2)); // result => {false=[a, bb, a], true=[ccc, dddd]}