Java map.copyOf未使用源映射比较器



我有无数带有自定义键和比较器的映射。我注意到,当我使用这样的代码创建地图时

var map = TreeMap<>( someCustomComparator );

然后,稍后我使用以下代码创建了一个不可变的(小而快速的)副本:

map = Map.copyOf( map );

然后,即使someObjectsimilarObject在比较器someCustomComparator下比较相等("具有相同的等价类"),map.get( similarObject )也无法检索someObject

调试到API中我发现Map.copyOf返回一个使用Object::equals比较键的映射实现,即它不使用用于构造原始映射的比较器(在我的示例中,这将是someCustomComparator)。显然,当someObjectsimilarObject不是同一对象,但在someCustomComparator下具有相同的等价类,但Object::equals没有被覆盖时,就会产生

map.get( similarObject ) ==> someObject

map = Map.copyOf( map )指令之前,以及

map.get( similarObject ) ==> null 

在CCD_ 14指令之后。这是我必须面对的预期行为,还是我应该报告Java缺陷?

(注意,some/similarObject类也实现comparableMap.copyOf实现也忽略了这一点。)

(我认为这种行为在所有集合copyOf实现中都很常见。)

copyOf方法的规范说明

返回一个不可修改的Map,该Map包含给定Map的条目。

(强调矿)

这意味着只有条目被复制,没有其他内容。因此,这是预期的行为,而不是bug。

另一方面,正如评论中所建议的,TreeMap类的构造函数的文档中说

构建一个新的树映射,该映射包含与指定排序映射相同的映射并使用相同的顺序。这种方法在线性时间内运行。

参数:

m-已排序的映射,其映射将放置在此映射中,并且其比较器将用于对此映射进行排序

(再次强调挖掘)

除了Federico在他的回答中所说的之外,Map的Javadoc在";不可修改的地图";区段:

  • 映射的迭代顺序未指定,可能会发生更改

如果需要使用相同顺序的不可修改映射,请使用Collections.unmodifiableMap包装该映射。如果你想要一个副本,只需创建一个新的TreeMap,正如费德里科所说:

private static <K, V> SortedMap<K, V> copyOf(SortedMap<K, V> map) {
return Collections.unmodifiableSortedMap(new TreeMap<>(map));
}

Map的API规范中,Map.get(Object key)规定:

";更正式地说,如果这个映射包含从密钥k到值v的映射,使得Objects.equals(key, k),那么这个方法返回v;否则返回CCD_ 28。(最多可以有一个这样的映射。)

SortedMap的API规范规定:

";插入到排序映射中的所有键必须实现CCD_ 30接口(或者被指定的比较器接受)">

";请注意,如果排序映射要正确实现Map接口,则排序映射(无论是否提供显式比较器)维护的排序必须与equals一致">

当我发布这个问题时,我忘记了后一段。因此,问题中提到的上述行为是意料之中的,即使它很奇怪。因此,为了在MapSortedMap之间获得一致的行为,必须重写Object::equals以使其与ComparableComparator一致,以使用的为准。

这是一个相当严重的要求/限制,因为这意味着您只能将带有比较器的映射与键结合使用,其中键的实现覆盖Object::equals,以与定制比较器的equals版本一致。也就是说,原则上,每个定制比较器都应该有一个相应的定制密钥。我认为这一点很容易被忽略;我当然错过了。

这一要求的必然结果是,没有专门设计为某些定制比较器的定制密钥的对象通常不能用作具有定制比较器的映射(继承SortedMap的东西/任何东西)的密钥,因为通常情况下,该映射将违反Map的合同。我觉得整个情况有问题。我认为对所有地图(包括Map)使用以下比较层次结构会更一致:

  • 如果存在,Comparator<T>,否则
  • 如果为提供的密钥类型Comparable<T>定义,则为
  • 如果为提供的密钥类型Object::equals重写,则为else
  • 使用默认的Object::equals,即==

即,如果已将比较器提供给映射,则使用它;否则,如果密钥是可比较的,则使用该比较器;否则,使用密钥的equal(Object)方法,其默认实现为==。不允许定制比较器(实际上任何不是SortedMap的东西)的Map的实现将需要接受比较器(并在没有提供比较器的情况下使用Comparable<T>),以定义等价类(而不是排序)。为此目的,定义";CCD_ 51";,或";CCD_ 52";以便为一些CCD_ 54中存在的CCD_。这避免了为不需要/不接受定制排序顺序的地图定义排序顺序的需要。比较器可以被修改为继承Equalator<T>,以提供显式等价和排序顺序(Comparator<T>通常意味着等价的隐式定义)。

MapAPI规范的这一更改将避免为SortedMaps的每个比较器定义定制密钥的需要,并允许在不违反Map合同的情况下使用通用对象作为密钥。这也意味着,相同的对象类型可以在不同的Map中用作密钥,同时使用不同的等价定义,这在目前是不容易实现的,因为Map被限制为使用Object::equals来定义等价。

我没有使用未排序映射的不同等价类的用例,但我有大量定制比较器的用例,有时有定制键,有时没有定制键,我需要为映射定义一个比较器,并在相关键中定义一个一致的Object::equals,这是有问题的。我认为,比较国应该是足够的。忘记定义,或者使Object::equals的定义与映射的比较器不一致,都很容易导致奇怪的缺陷,这些缺陷可能在远离原因的地方表现出来,需要API调试才能理解。

最新更新