Search code examples
javajava-8mergecollectors

In Java8, when will the merge function be triggered in Collectors.toMap?


I want to transform a Hashmap<String,Long> to a Treemap, in order to sort its key by string.length (I can't simply use treemap.addAll because I have may other logic when insert and I want to use java8)

The code is below. But when keys with same length exist in the initial Hashmap, it will trigger the merge function which throws Exception(I intent to do it because there won't be same string in my case). I wonder why the merge function be triggered since the JavaDoc of toMap() says "If the mapped keys contains duplicates (according to Object#equals(Object)), the value mapping function is applied to each equal element, and results are merged using the provided merging function." I think that in my code the "mapped keys" should be the entry in hashMap mapped by Entry::getKey but not string.length() in the TreeMap comparator. i.e. "abc" != "def". So it shouldn't trigger the merge. But?? What the hell?

public class TestToMap {

    public static Map<String, Long> map1 = new HashMap<String, Long>() {
        {
            put("abc", 123L);
            put("def", 456L);
        }
    };

    public static void main(String[] args) {
        Map<String, Long> priceThresholdMap = map1.entrySet().stream()
            .collect(Collectors.toMap(Entry::getKey,
                                      Entry::getValue,
                                      throwingMerger(),
                                      () -> new TreeMap<String, Long>(
                                          (a, b) -> {
                                              return a.length() - b.length();
                                          }))); // this will trigger merge function, why?
        //() -> new TreeMap<String, Long>(Comparator.comparingInt(String::length).thenComparing(String::compareTo))));  // but this won't trigger merge function 

    }

    private static <T> BinaryOperator<T> throwingMerger() {
        return (u, v) -> {
            throw new IllegalStateException(String.format("priceThresholdMap has duplicate v1 %s,v2 %s", u, v));
        };
    }
}

Solution

  • According to toMap() source code, it create a accumulator which will fold each element from source stream into map.

    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction,
                                    Supplier<M> mapSupplier) {
            BiConsumer<M, T> accumulator
                    = (map, element) -> map.merge(keyMapper.apply(element),
                                                  valueMapper.apply(element), mergeFunction);
            return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
        }
    

    And in Map.merge() , when get("def") will return the exist oldValue=123, which key is "abc", because by the comparator I give to TreeMap "def" is equal to "abc". And then oldValue!=null calls merge function.

     default V merge(K key, V value,
                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); // call the merge function
            if(newValue == null) {
                remove(key);
            } else {
                put(key, newValue);
            }
            return newValue;
        }
    

    ref:Collectors toMap duplicate key