【问题标题】:In Java8, when will the merge function be triggered in Collectors.toMap?在Java8中,Collectors.toMap什么时候会触发合并功能?
【发布时间】:2020-02-20 07:03:35
【问题描述】:

我想将Hashmap<String,Long> 转换为Treemap,以便按string.length 对其键进行排序(我不能简单地使用treemap.addAll,因为插入时我可能有其他逻辑并且我想使用java8)

代码如下。但是当初始 Hashmap 中存在相同长度的键时,它会触发合并函数,该函数会抛出异常(我打算这样做,因为在我的情况下不会有相同的字符串)。我想知道为什么会触发合并函数,因为 toMap() 的 JavaDoc 说“如果 映射键 包含重复项(根据 Object#equals(Object)),则值映射函数应用于每个相等元素,并使用提供的合并功能合并结果。”我认为在我的代码中,“映射键”应该是由 Entry::getKey 映射的 hashMap 中的条目,而不是 TreeMap 比较器中的 string.length()。即“abc”!=“def”。所以它不应该触发合并。但??什么鬼?

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));
        };
    }
}

【问题讨论】:

    标签: java java-8 merge collectors


    【解决方案1】:

    当然它应该触发合并。合并函数用于合并输出Map 中具有相同键的值,在您的情况下为TreeMap

    TreeMap中,如果Comparatorcompare方法返回0,则key是相同的,所以两个长度相同的key被认为是相同的,它们对应的值应该被合并。

    请注意,您的Comparator 导致输出TreeMap 无法正确实现Map 接口,因为它定义的顺序与equals() 不一致:

    请注意,树形图维护的顺序与任何排序图一样,并且 无论是否提供显式比较器,都必须保持一致 如果此排序映射要正确实现 地图界面

    (来自TreeMapJavadoc)

    如果要对String的长度进行排序,仍然可以和equals保持一致:

    代替

    return a.length() - b.length()
    

    使用

    return a.length() == b.length() ? a.compareTo(b) : Integer.compare(a.length(),b.length())
    

    现在具有相同长度的不相等Strings 将按字典顺序排序,而具有不同长度的String 将按长度排序。

    【讨论】:

    • 为了后代:您的一致与等于比较器也可以使用comparingInt(String::length).thenComparing(naturalOrder()) 来实现。
    【解决方案2】:

    根据 toMap() 源代码,它创建了一个累加器,它将源流中的每个元素折叠到地图中。

    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);
        }
    

    而在 Map.merge() 中,当 get("def") 将返回存在的 oldValue=123 时,它的键是 "abc",因为通过比较器我给 TreeMap "def" 等于 "abc" .然后 oldValue!=null 调用合并函数。

     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;
        }
    

    参考:Collectors toMap duplicate key

    【讨论】:

    • 您应该查看the documentation,而不是查看源代码。或阅读其他答案。
    • 从文档中您实际上并不知道源流中的每个元素都将被传递到 Map.merge() 中,然后调用 TreeMap 中的比较器,然后调用自定义合并函数。文档只说当映射键重复时将调用自定义合并函数,但何时以及由谁判断重复是隐含的。我认为重复判断应该在key-mapping之后,插入到TreeMap之前进行,这很容易出错
    • 否则它应该如何工作?你得到一个 map 作为结果,并且 map 不能有重复的键(根据它自己的相等定义)。所以地图对关键相等性的定义很重要。当你不合并 "abc""def" 的值时,因为你不认为它们是相等的,而是生成一个只能有其中一个键的结果映射,会发生什么?
    • 是的,你是对的。我只是不知道是谁触发了合并,因为 TreeMap 中没有合并功能,当 TreeMap.put 遇到具有重复键的值时,它只是替换旧值。从 toMap() 源代码我知道这项工作是通过调用 Map.merge() 在累加器中完成的。
    • TreeMap 有一个merge 方法;它是从Map 接口继承的。
    猜你喜欢
    • 1970-01-01
    • 2010-11-18
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多