[ Java8 ] Stream Collectors toMap 사용시 Duplicate Key Error

박상준·2024년 3월 7일
1

JAVA

목록 보기
3/5
post-custom-banner

[ Java8 ] Stream Collectors toMap 중복키처리하기(Duplicate Key error)

  • 와 관련된 문제가 발생할 수 있다는 것을 알게 되었습니다.

에러의 원인

  • 해당 에러는 Collection.toMap() 함수에서 key 가 중복되는 경우 어떻게 처리하는지에 그 소스 자체의 정책에 따라 에러가 발생한다고 합니다.
  • 한번 내부 소스를 따라가 본다면,

Collectors.class

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                Function<? super T, ? extends U> valueMapper) {
    return new CollectorImpl<>(HashMap::new,
                               uniqKeysMapAccumulator(keyMapper, valueMapper),
                               uniqKeysMapMerger(),
                               CH_ID);
}
  • toMap 을 사용하는 경우
    Map<Long, ProductEntity> productMap = foundProducts.stream()
          .collect(Collectors.toMap(ProductEntity::getId, product -> product));
    • 왼쪽 파라미터가 keyMapper

      • 입력 요소를 맵의 키로 매핑하는 함수의 역할.
    • 오른쪽 파라미터가 valueMapper

      • 입력 요소를 맵의 값으로 매핑하는 함수의 역할을 한다.
    • 각 입력 요소를 uniqKeysMapAccumulator 메서드를 통해

      
      private static <T, K, V> BiConsumer<Map<K, V>, T> uniqKeysMapAccumulator(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends V> valueMapper) {
          return (map, element) -> {
              K k = keyMapper.apply(element);
              V v = Objects.requireNonNull(valueMapper.apply(element));
              V u = map.putIfAbsent(k, v);
              if (u != null) throw duplicateKeyException(k, u, v);
          };
      }
    • 내부적으로 Map<K,V> 타입의 맵과 T 타입의 요소를 반환한다.

      • 반환된 BiConsumer 는 스트림의 각 요소에 대해
      1. keyMapper 에서 키를 추출
      2. valueMapper 요소에서 값을 추출
      3. 추출된 키와 밸류의 NULL 체크
      4. 해당 키가 맵에 존재한다면 기존 값을 반환
      5. 기존값 u 가 ≠ null 이라면
        • 이미 해당 키가 존재하기에, duplicateKeyException 가 발생한다.

그래서 (p1, p2) -> p1 를 해주는 이유가?

  • (p1, p2) -> p1 구문은 충돌이 발생한 경우
    • 즉, 같은 key를 가진 값들이 존재하는 경우 어떤 값을 선택할지를 정의함.
    • p1 과 p2 중에 난 p1 을 선택하겠어 라는 의미를 전달합니다.
  • 사용 소스에서는
    		Map<Long, ProductEntity> productMap = foundProducts.stream()
    			.collect(Collectors.toUnmodifiableMap(ProductEntity::getId, product -> product, (p1, p2) -> p1));
    • 이런식으로 수정하고,

    • 사용되는 메서드는 이런 메서드로 변경이 됩니다

      
      
      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) {
          BiConsumer<M, T> accumulator
                  = (map, element) -> map.merge(keyMapper.apply(element),
                                                valueMapper.apply(element), mergeFunction);
          return new CollectorImpl<>(mapFactory, accumulator, mapMerger(mergeFunction), CH_ID);
      }
    • merge 메서드안에서

      default V merge(K key, V valㅁue,
              BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
          Objects.requireNonNull(remappingFunction);
          Objects.requireNonNull(value);
          V oldValue = get(key); // 추가하는 키값
          V newValue = (oldValue == null) ? value : 
                     remappingFunction.apply(oldValue, value);
      		// 만약 이전 키에 대한 값이 존재하지 않으면 그냥 newKey, newValue 값으로 맵에 추가
      		// 이전 키에 대한 값이 존재한다면, remappingFunction 으로 (p1 , p2 ) -> p1 ... oldValue 에 대한 값을 반환한다.
      		// 즉 , key, value 에는 이전에 존재하던 값이 그대로 유지됩니다.
          if (newValue == null) {
              remove(key);
          } else {
              put(key, newValue);
          }
          return newValue;
      }
profile
이전 블로그 : https://oth3410.tistory.com/
post-custom-banner

0개의 댓글