(자바) Collectos.toMap()의 다중 사용

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}