☕️ Java/기본

[Java] Collectors.toMap() 의 여러 가지 사용법

말 랑 2023. 3. 14. 00:05
728x90

 

 

 

 

 

🧐 Collectors.toMap이 뭔가요?

Stream을 사용하는 경우, 최종 연산으로 collect()를 사용하는 경우, Stream의 요소들을 수집하여 특정한 자료구조로 변환할 수 있습니다.


이때 Map으로 변환하기 위해서는 Collectors.toMap을 사용합니다.

 

흔히 사용하는 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}

그러나 위 코드에는 문제점이 있습니다.

 

바로 key의 중복이 발생한다면, 오류가 발생한다는 것인데요, 살펴보도록 하겠습니다.

List<String> strings = Arrays.asList("apple", "banana", "carrot", "pear");

Map<Integer, String> map = strings.stream()
        .collect(Collectors.toMap(String::length, Function.identity()));

발생하는 오류는 다음과 같습니다.

Duplicate key 6 (attempted merging values banana and carrot) java.lang.IllegalStateException: Duplicate key 6 (attempted merging values banana and carrot)

이제 이를 해결해 보도록 하겠습니다.






 

 

 

🧐 toMap() 시 Key의 중복을 해결하기

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보다, 파라미터가 1개 더 늘어났으며, 해당 파라미터의 이름은 mergeFunction이라 적혀있습니다.




 

 

 

 

📕 mergeFunction() 이 뭔가요?

toMap의 mergeFunction매개변수는 다음과 같이 설명되어 있습니다.

a merge function, used to resolve collisions between values associated with the same key, as supplied to Map.merge(Object, Object, BiFunction)

즉 mergeFunction은 동일한 키로 인해 충돌(collision)이 발생했을 때, 어떠한 value를 취할 것인지 결정할 때 사용됩니다.

 

예를 들어, 무조건 새로운 값으로 덮어쓰려는 경우 다음과 같이 작성할 수 있습니다.

(existingValue, newValue) -> newValue;

이제 mergeFunction을 사용하여, 위에서 발생했던 Key의 충돌로 인한 오류를 해결해 보도록 하겠습니다.

 

아래는 충돌 발생 시, 이전의 값을 새로 들어온 값으로 대체하는 코드입니다.

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 이외의 다른 Map을 사용하는 방법

위의 예시에서 반환되는 Map은 항상 hashMap입니다.


그러나 예를 들어, Key의 순서를 유지하고 싶은 경우 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) { 
}

마지막 파라미터로 Supplier<M> mapFactory가 추가되었는데요, 이에 대한 설명은 다음과 같습니다.

mapFactory – a supplier providing a new empty Map into which the results will be inserted

즉 반환될 때 사용할 empty Map을 제공하여야 합니다.

 

예를 들어 순서를 유지하기 위해 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에는 empty Map을 제공해야 한다고 되어있지만, 꼭 그럴 필요는 없습니다.

 


즉, 아래와 같이 비어있지 않은 값을 제공할 수 있습니다.

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}

 

 

 

 

 

 

 

 

 

728x90