不区分大小写的比较器破坏了我的TreeMap。

21

我在我的TreeMap中使用的一个Comparator破坏了我想要的TreeMap行为。请看以下代码:

TreeMap<String, String> treeMap = new TreeMap<>(new Comparator<String>() {
    public int compare(String o1, String o2) {
        return o1.toLowerCase().compareTo(o2.toLowerCase());
    }
});
treeMap.put("abc", "Element1");
treeMap.put("ABC", "Element2");

我认为我做的是创建了一个按键排序且不区分大小写的地图。这两个不同的元素具有非等键(abcABC),它们的比较将返回0。我期望只是两个元素的随机排序。然而,命令是:

System.out.println("treeMap: " + treeMap);
导致:
treeMap: {abc=Element2}

关键字abc已被重新赋值为Element2的值!

有人能解释一下这是怎么发生的,以及是否是TreeMap的有效文档行为吗?


Comparator 是一个全序。对于 "abc" 和 "ABC" 在你的映射中是什么意思呢?哪个先出现? - Reinstate Monica
3
你已经获得了预期的(尽管意料之外的)结果;你的比较器是不区分大小写的,你的映射使用了这个比较器 - 因此,在键方面,它也是不区分大小写的。你刚刚想出了如何创建一个不区分大小写的映射 - 表扬你,但我在这里看不到任何“令人震惊”或“非法”的事情...它正在做你告诉它要做的事情。 - user719662
4个回答

36
这是因为TreeMap认为如果a.compareTo(b) == 0,则这两个元素相等。在 TreeMap的JavaDoc 中有记录(强调是我的):
请注意,由树映射(如任何排序映射一样)维护的排序,无论是否提供了显式比较器,都必须与equals一致,如果这个排序映射要正确实现Map接口。 (有关一致性的精确定义,请参见Comparable或Comparator。)这是因为Map接口是根据equals操作定义的,但是排序映射使用其compareTo(或compare)方法执行所有键比较,因此从排序映射的角度来看,被视为相等的两个键是相等的。
您的比较器与equals不一致。
如果要保留不相等但忽略大小写的元素,则将第二级检查插入到比较器中,以使用区分大小写的排序:
    public int compare(String o1, String o2) {
        int cmp = o1.compareToIgnoreCase(o2);
        if (cmp != 0) return cmp;

        return o1.compareTo(o2);
    }

{btsdaf} - javaxian
1
{btsdaf} - JAB
7
@JAB: 我不确定我同意。只要你遵守其接口的保证,TreeMap 类符合 Map 接口的保证!它的文档清楚地说明比较器必须与 equals 一致。同样,如果equalshashCode不一致,任何 HashMap 都会表现不良,您是否也认为这“不太好”?我的观点是:契约双方都需要遵守。 - wchargin
{btsdaf} - Andy Turner
{btsdaf} - corsiKa
@LordFarquaad 我的意思是,编译器无法自动强制实施一致性,这并不好。编译器要做到这一点是非常困难的,但我敢打赌,这样的强制执行在多年来肯定能够节省很多压力和痛苦。 - Andy Turner

12
您传递给 TreeMapComparator 不仅决定了 Map 内部键的顺序,还决定了两个键是否被视为相同(当 compare() 返回 0 时它们被视为相同)。

因此,在您的 TreeMap 中,"abc" 和 "ABC" 被认为是相同的键。 Map 不允许有相同的键,因此第二个值 Element2 覆盖了第一个值 Element1


4

您需要确保该地图元素的相等性与比较器一致。引用自类注释:

请注意,树形地图维护的排序(与任何排序的地图一样,无论是否提供显式比较器)必须与等于号一致,如果要正确实现接口,则必须保持一致。


1

5
开发人员应该使用现有的比较器或专用于比较的方法,而不是昂贵的“转换为小写字母”的反模式。但这并不能解决问题。你仍然需要一个两步比较器。Java 8的解决方案是String.CASE_INSENSITIVE_ORDER.thenComparing(Comparator.naturalOrder()) - Holger
@AndyTurner,你说得非常对,使用内置比较器(或者在这种情况下使用Collator)是比编写自己的比较器更好的选择。然而,在我的生产代码中,我使用表示顺序整数字段来比较对象,而这些字段在某些情况下具有相同的值。我不想让我的示例过于复杂,所以我选择了一个不区分大小写的字符串比较器。 - javaxian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接