Collectors.toMap이 무엇인가요?
스트림을 사용할 때 마지막 작업으로 collect()를 사용하여 스트림의 요소를 수집하고 특정 데이터 구조로 변환할 수 있습니다.
이 시점에서 Collectors.toMap은 Map으로 변환하는 데 사용됩니다.
일반적으로 사용되는 toMap은 다음과 같습니다.
List<String> strings = Arrays.asList("apple", "banana", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(String::length, Function.identity()));
System.out.println(map); // 결과: {4=pear, 5=apple, 6=banana}
그러나 위의 코드에는 문제가 있습니다.
중복 키가 발생하면 오류가 발생합니다. 한 번 보자.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(String::length, Function.identity()));
내가 얻는 오류는 다음과 같습니다.
중복 키 6(바나나와 당근 값 병합 시도) java.lang.IllegalStateException: 중복 키 6(바나나와 당근 값 병합 시도)
이제 이를 해결해보도록 하겠습니다.
toMap()에서 중복 키 해결
toMap()은 서로 다른 서명을 가지고 있으며 중복 키는 다음 서명에 따라 toMap()으로 해결할 수 있습니다.
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
// ...
}
기존에 사용하던 toMap에 비해 또 다른 파라미터가 추가되었으며 파라미터 이름은 병합 기능쓰여지 다.
mergeFunction()이 무엇인가요?
toMap 매개변수 mergeFunction은 다음과 같이 설명됩니다.
Map.merge(Object, Object, BiFunction)에 제공된 것과 동일한 키에 매핑된 값 간의 충돌을 해결하는 데 사용되는 병합 함수
즉, mergeFunction은 동일한 키로 인해 충돌이 발생했을 때 어떤 값을 취할지 결정하는 데 사용됩니다.
예를 들어 새 값으로 무조건 덮어쓰려면 다음과 같이 작성할 수 있습니다.
(existingValue, newValue) -> newValue;
이제 mergeFunction을 사용하여 위에서 발생한 키 충돌로 인한 오류를 수정해 보겠습니다.
아래는 충돌이 발생했을 때 이전 값을 새 값으로 바꾸는 코드입니다.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(oldVal, newVal) -> newVal
));
System.out.println(map); // {4=pear, 5=apple, 6=carrot}
첫 번째 입력 값을 유지하려면 다음과 같이 작성할 수 있습니다.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(oldVal, newVal) -> oldVal
));
System.out.println(map); // {4=pear, 5=apple, 6=banana}
또는 다음과 같은 값을 반환할 수 있습니다.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(oldVal, newVal) -> "말랑"
));
System.out.println(map); // {4=pear, 5=apple, 6=말랑}
toMap()에서 다른 맵을 HashMap으로 사용하는 방법
위의 예에서 반환된 맵은 항상 hashMap입니다.
그러나 예를 들어 키의 순서를 유지하려면 LinkedHashMap 등이 사용됩니다.
이 경우 다음 서명이 있는 toMap()을 사용할 수 있습니다.
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapFactory) {
}
마지막 매개변수로 공급자
mapFactory – 결과가 삽입될 새로운 빈 맵을 제공하는 제공자
즉, 반환 시 사용할 빈 카드를 제공해야 합니다.
예를 들어 LinkedHashMap을 사용하여 순서를 유지하려는 경우 이전 코드를 다음과 같이 변경할 수 있습니다.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(oldVal, newVal) -> newVal,
LinkedHashMap::new
));
System.out.println(map); // {5=apple, 6=carrot, 4=pear}
API는 빈 지도를 제공해야 한다고 말하지만 그럴 필요는 없습니다.
즉, 다음과 같이 비어 있지 않은 값을 지정할 수 있습니다.
List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");
Map<Integer, String> notEmptyMap = new LinkedHashMap<>();
notEmptyMap.put(100, "안녕하세요? 말랑입니다.");
Map<Integer, String> map = strings.stream()
.collect(Collectors.toMap(
String::length,
Function.identity(),
(oldVal, newVal) -> newVal,
() -> notEmptyMap
));
System.out.println(map); // {100=안녕하세요? 말랑입니다., 5=apple, 6=carrot, 4=pear}