Java 8 Collectors
1. Stream.collect() 메서드
Stream.collect()
는 Java 8의 Stream API 터미널 메소드 중 하나이다. 이를 통해 Stream 인스턴스에 있는 데이터 요소에 대해 변경 가능한 접기 작업(요소를 일부 데이터 구조에 다시 패키징하고 추가 논리를 적용하고 연결하는 등)을 수행할 수 있다.
이 작업에 대한 전략은 Collector 인터페이스 구현을 통해 제공된다.
2. Collectors
사전 정의된 모든 구현은 Collectors 클래스에서 찾을 수 있다. 향상된 가독성을 활용하기 위해 다음과 같은 정적 가져오기를 사용하는 것이 일반적인 관행이다.
1
import static java.util.stream.Collectors.*;
원하는 단일 가져오기 수집기를 사용할 수도 있다.
1
2
3
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
다음 예에서는 다음 목록을 재사용한다.
1
List<String> givenList = Arrays.asList("a", "bb", "ccc", "dd");
1) Collectors.toList()
toList 수집기는 모든 Stream 요소를 List 인스턴스로 수집하는데 사용할 수 있다. 중요한 점은 이 메소드를 사용하여 특정 List 구현을 가정할 수 없다는 것이다. 이에 대해 더 많은 제어를 원할 경우 toCollection을 대신 사용할 수 있다.
일련의 요소를 나타내는 Stream 인스턴스를 만든 다음 이를 List 인스턴스로 수집해 본다.
1
List<String> result = givenList.stream().collect(toList());
2) Collectors.toUnmodifiableList()
Java 10에서는 Stream 요소를 수정 불가능한 목록 에 누적하는 편리한 방법을 도입했다.
1
List<String> result = givenList.stream().collect(toUnmodifiableList());
이제 결과 List를 수정하려고 하면 UnsupportedOperationException이 발생한다.
1
assertThatThrownBy(() -> result.add("foo")).isInstanceOf(UnsupportedOperationException.class);
3) Collectors.toSet()
toSet 콜렉터는 모든 Stream 요소를 Set 인스턴스 로 수집하는데 사용할 수 있다. 중요한 점은 이 메서드를 사용하여 특정 Set 구현을 가정할 수 없다는 것이다. 이에 대해 더 많은 제어를 원할 경우 toCollection을 대신 사용할 수 있다.
일련의 요소를 나타내는 Stream 인스턴스를 만든 다음 이를 Set 인스턴스로 수집해 본다.
1
Set<String> result = givenList.stream().collect(toSet());
Set에는 중복 요소가 포함되어 있지 않다. 컬렉션에 서로 동일한 요소가 포함된 경우 해당 요소는 결과 Set에 한 번만 나타난다.
1
2
3
List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
Set<String> result = listWithDuplicates.stream().collect(toSet());
assertThat(result).hasSize(4);
4) Collectors.toUnmodifyingSet()
Java 10부터 toUnmodifyingSet()
컬렉터를 사용하여 수정 불가능한 Set을 쉽게 생성할 수 있다.
1
Set<String> result = givenList.stream().collect(toUnmodifiableSet());
결과 세트를 수정하려는 모든 시도는 UnsupportedOperationException으로 종료된다.
1
assertThatThrownBy(() -> result.add("foo")).isInstanceOf(UnsupportedOperationException.class);
5) Collectors.toCollection()
toSet 및 toList 수집기를 사용할 때 해당 구현에 대해 어떠한 가정도 할 수 없다. 사용자 정의 구현을 사용하려면 선택한 컬렉션을 제공하는 toCollection 수집기를 사용해야 한다.
일련의 요소를 나타내는 Stream 인스턴스를 만든 다음 이를 LinkedList 인스턴스로 수집해 본다.
1
List<String> result = givenList.stream().collect(toCollection(LinkedList::new))
이는 변경 불가능한 컬렉션에서는 작동하지 않는다. 그러한 경우에는 사용자 정의 Collector 구현을 작성하거나 collectionAndThen을 사용해야 한다.
6) Collectors.toMap()
toMap 수집기는 Stream 요소를 Map 인스턴스로 수집하는데 사용할 수 있다. 이를 위해서는 두 가지 기능을 제공해야 한다.
keyMapper
valueMapper
keyMapper를 사용하여 Stream 요소에서 Map 키를 추출 하고 valueMapper를 사용하여 특정 키와 관련된 값을 추출한다.
문자열을 키로, 길이를 값으로 저장하는 Map으로 해당 요소를 수집해 본다.
1
Map<String, Integer> result = givenList.stream().collect(toMap(Function.identity(), String::length))
Function.identity()
는 동일한 값을 받아들이고 반환하는 함수를 정의하는 간단한 방법이다.
그렇다면 컬렉션에 중복된 요소가 포함되어 있으면 toSet과 달리 toMap은 중복 항목을 자동으로 필터링하지 않는다. 이는 이 키에 대해 어떤 값을 선택할지 어떻게 알아낼 수 있기 때문에 이해할 수 있는 것이다.
1
2
3
4
List<String> listWithDuplicates = Arrays.asList("a", "bb", "c", "d", "bb");
assertThatThrownBy(() -> {
listWithDuplicates.stream().collect(toMap(Function.identity(), String::length));
}).isInstanceOf(IllegalStateException.class);
toMap은 값이 동일한지 여부도 평가하지 않는다. 중복된 키가 발견되면 즉시 IllegalStateException이 발생한다.
키 충돌이 있는 경우에는 다른 서명과 함께 toMap을 사용해야 한다.
1
Map<String, Integer> result = givenList.stream().collect(toMap(Function.identity(), String::length, (item, identicalItem) -> item));
여기서 세 번째 인수는 충돌 처리 방법을 지정할 수 있는 BinaryOperator 이다. 이 경우 동일한 문자열은 항상 동일한 길이를 갖기 때문에 충돌하는 두 값 중 하나를 선택한다.
7) Collectors.toUnmodifyingMap()
List 및 Set과 마찬가지로 Java 10에서는 Stream 요소를 수정 불가능한 Map으로 수집하는 쉬운 방법을 도입했다.
1
Map<String, Integer> result = givenList.stream().collect(toUnmodifiableMap(Function.identity(), String::length))
Map에 새 항목을 넣으려고 하면 UnsupportedOperationException이 발생한다.
1
assertThatThrownBy(() -> result.put("foo", 3)).isInstanceOf(UnsupportedOperationException.class);
8) Collectors.collectingAndThen()
CollectingAndThen은 끝을 수집한 직후 결과에 대해 다른 작업을 수행할 수 있는 특수 수집기이다.
Stream 요소를 List 인스턴스로 수집한 다음 결과를 ImmutableList 인스턴스로 변환해 본다.
1
List<String> result = givenList.stream().collect(collectingAndThen(toList(), ImmutableList::copyOf))
9) Collectors.joining()
Joining 컬렉터는 Stream<String>
요소를 조인하는데 사용될 수 있다.
우리는 다음을 수행하여 그것들을 함께 결합할 수 있다.
1
String result = givenList.stream().collect(joining());
결과는 다음과 같다.
1
"abbcccdd"
사용자 정의 구분 기호, 접두사, 접미사를 지정할 수도 있다.
1
String result = givenList.stream().collect(joining(" "));
결과는 다음과 같다.
1
"a bb ccc dd"
다음과 같이 작성할 수도 있다.
1
String result = givenList.stream().collect(joining(" ", "PRE-", "-POST"));
결과는 다음과 같다.
1
"PRE-a bb ccc dd-POST"
10) Collectors.counting()
Counting은 모든 Stream 요소의 계산을 허용하는 간단한 수집기이다.
이제 다음과 같이 작성할 수 있다.
1
Long result = givenList.stream().collect(counting());
11) Collectors.summarizingDouble/Long/Int()
SummarizingDouble/Long/Int는 추출된 요소 스트림의 수치 데이터에 대한 통계 정보가 포함된 특수 클래스를 반환하는 수집기이다.
다음을 수행하여 문자열 길이에 대한 정보를 얻을 수 있다.
1
DoubleSummaryStatistics result = givenList.stream().collect(summarizingDouble(String::length));
이 경우 다음이 적용된다.
1
2
3
4
5
assertThat(result.getAverage()).isEqualTo(2);
assertThat(result.getCount()).isEqualTo(4);
assertThat(result.getMax()).isEqualTo(3);
assertThat(result.getMin()).isEqualTo(1);
assertThat(result.getSum()).isEqualTo(8);
12) Collectors.averagingDouble/Long/Int()
AveragingDouble/Long/Int는 추출된 요소들의 평균을 단순히 반환하는 컬렉터이다.
다음을 수행하여 평균 문자열 길이를 얻을 수 있다.
1
Double result = givenList.stream().collect(averagingDouble(String::length));
13) Collectors.summingDouble/Long/Int()
SummingDouble/Long/Int는 추출된 요소의 합을 단순히 반환하는 컬렉터이다.
다음을 수행하여 모든 문자열 길이의 합계를 얻을 수 있다.
1
Double result = givenList.stream().collect(summingDouble(String::length));
14) Collectors.maxBy()/minBy()
MaxBy/MinBy 수집기는 제공된 Comparator 인스턴스에 따라 Stream의 가장 큰/가장 작은 요소를 반환한다.
다음을 수행하여 가장 큰 요소를 선택할 수 있다.
1
Optional<String> result = givenList.stream().collect(maxBy(Comparator.naturalOrder()));
반환된 값이 Optional 인스턴스에 래핑되어 있음을 볼 수 있다. 이로 인해 사용자는 빈 컬렉션 코너 케이스를 다시 생각하게 된다.
15) Collectors.groupingBy()
GroupingBy 컬렉터는 일부 속성을 기준으로 객체를 그룹화한 다음 결과를 Map 인스턴스에 저장하는데 사용된다.
문자열 길이별로 그룹화하고 그룹화 결과를 Set 인스턴스에 저장할 수 있다.
1
Map<Integer, Set<String>> result = givenList.stream().collect(groupingBy(String::length, toSet()));
결과는 다음과 같다.
1
2
3
4
assertThat(result)
.containsEntry(1, newHashSet("a"))
.containsEntry(2, newHashSet("bb", "dd"))
.containsEntry(3, newHashSet("ccc"));
groupingBy 메소드의 두 번째 인수가 Collector임을 알 수 있다. 또한 원하는 수집기를 자유롭게 사용할 수 있다.
16) Collectors.partitioningBy()
PartitioningBy는 Predicate 인스턴스를 허용한 다음 부울 값을 키로 저장하고 컬렉션을 값으로 저장하는 Map 인스턴스로 Stream 요소를 수집하는 groupingBy의 특수한 사례이다. “true” 키 아래에서는 주어진 Predicate와 일치하는 요소 모음을 찾을 수 있고, “false” 키 아래에서는 주어진 Predicate와 일치하지 않는 요소 모음을 찾을 수 있다.
다음과 같이 쓸 수 있다.
1
Map<Boolean, List<String>> result = givenList.stream().collect(partitioningBy(s -> s.length() > 2))
결과적으로 다음을 포함하는 Map이 생성된다.
1
{false=["a", "bb", "dd"], true=["ccc"]}
17) Collectors.teeing()
지금까지 배운 수집기를 사용하여 특정 스트림에서 최대값과 최소값을 찾아보겠다.
1
2
3
4
List<Integer> numbers = Arrays.asList(42, 4, 2, 24);
Optional<Integer> min = numbers.stream().collect(minBy(Integer::compareTo));
Optional<Integer> max = numbers.stream().collect(maxBy(Integer::compareTo));
// do something useful with min and max
여기서는 두 개의 서로 다른 수집기를 사용한 다음 이 두 가지의 결과를 결합하여 의미 있는 것을 만든다. Java 12 이전에는 이러한 사용 사례를 처리하기 위해 지정된 스트림에서 두 번 작업하고 중간 결과를 임시 변수에 저장한 다음 나중에 해당 결과를 결합해야 했다.
다행스럽게도 Java 12는 이러한 단계를 처리하는 내장 수집기를 제공한다. 두 개의 수집기와 결합기 기능을 제공하는 것뿐이다.
이 새로운 컬렉터는 주어진 스트림을 서로 다른 두 방향으로 teeing 하므로 이를 teeing이라고 한다.
1
2
3
4
5
numbers.stream().collect(teeing(
minBy(Integer::compareTo), // The first collector
maxBy(Integer::compareTo), // The second collector
(min, max) -> // Receives the result from those collectors and combines them
));
- 맞춤형 Collectors 자체 Collector 구현을 작성하려면 Collector 인터페이스를 구현하고 세 가지 일반 매개변수를 지정해야 합니다.
1
public interface Collector<T, A, R> {...}
T - 수집에 사용할 수 있는 객체 유형
A - 변경 가능한 누산기 객체의 유형
R - 최종 결과의 유형
ImmutableSet 인스턴스에 요소를 수집하기 위한 예제 Collector를 작성해 본다. 올바른 유형을 지정하는 것부터 시작한다.
1
private class ImmutableSetCollector<T> implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {...}
내부 컬렉션 작업 처리를 위해 변경 가능한 컬렉션이 필요하므로 ImmutableSet을 사용할 수 없다. 대신, 변경 가능한 다른 컬렉션이나 일시적으로 객체를 축적할 수 있는 다른 클래스를 사용해야 한다. 이 경우 ImmutableSet.Builder
를 사용 하고 이제 5가지 메서드를 구현해야 한다.
Supplier<ImmutableSet.Builder<T>> supplier()
BiConsumer<ImmutableSet.Builder<T>, T> accumulator()
BinaryOperator<ImmutableSet.Builder<T>> combiner()
Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher()
Set<Characteristics> characteristics()
supplier() 메서드는 빈 Supplier 인스턴스를 생성하는 공급자 인스턴스를 반환한다. 따라서 이 경우 간단히 다음과 같이 작성할 수 있다.
1
2
3
4
@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
return ImmutableSet::builder;
}
accumulator()
메서드는 기존 accumulator 개체에 새 요소를 추가하는데 사용되는 함수를 반환한다. 이제 Builder의 add 메소드를 사용해 본다.
1
2
3
4
@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
return ImmutableSet.Builder::add;
}
combiner()
메서드는 두 개의 accumulators를 병합하는데 사용되는 함수를 반환한다.
1
2
3
4
@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
return (left, right) -> left.addAll(right.build());
}
finisher()
메서드는 누산기를 최종 결과 유형으로 변환하는데 사용되는 함수를 반환한다. 따라서 이 경우에는 Builder의 빌드 방법을 사용한다.
1
2
3
4
@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
return ImmutableSet.Builder::build;
}
characteristics()
메서드는 Stream에 내부 최적화에 사용될 몇 가지 추가 정보를 제공하는데 사용된다. 이 경우 Characteristics.UNORDERED
를 사용하기 때문에 Set의 요소 순서에 주의를 기울이지 않는다.
1
2
3
@Override public Set<Characteristics> characteristics() {
return Sets.immutableEnumSet(Characteristics.UNORDERED);
}
사용법과 함께 완전한 구현은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ImmutableSetCollector<T>
implements Collector<T, ImmutableSet.Builder<T>, ImmutableSet<T>> {
@Override
public Supplier<ImmutableSet.Builder<T>> supplier() {
return ImmutableSet::builder;
}
@Override
public BiConsumer<ImmutableSet.Builder<T>, T> accumulator() {
return ImmutableSet.Builder::add;
}
@Override
public BinaryOperator<ImmutableSet.Builder<T>> combiner() {
return (left, right) -> left.addAll(right.build());
}
@Override
public Function<ImmutableSet.Builder<T>, ImmutableSet<T>> finisher() {
return ImmutableSet.Builder::build;
}
@Override
public Set<Characteristics> characteristics() {
return Sets.immutableEnumSet(Characteristics.UNORDERED);
}
public static <T> ImmutableSetCollector<T> toImmutableSet() {
return new ImmutableSetCollector<>();
}
마지막으로 실제로 실행되는 내용은 다음과 같다.
1
2
3
List<String> givenList = Arrays.asList("a", "bb", "ccc", "dddd");
ImmutableSet<String> result = givenList.stream().collect(toImmutableSet());