原创

TreeSet 接上 Collator 后,排序规则也成了去重规则

三个字符串 e、é、E 都放进集合,最后只剩一个。若集合是带 Collator.PRIMARY 比较器的 TreeSet,这可以是排序规则正常生效的结果,而不是Java把字符弄丢了。

问题在于这份比较器同时承担了两种职责:决定显示顺序,以及决定集合中的元素是否相同。自然语言排序可以合理地忽略大小写和某些重音,业务身份却未必允许这样合并。

下面固定 Java 17、美国英语 Collator 和 PRIMARY 强度。地区与强度必须同时写清楚:不同语言的主次差异并没有统一的字母表规则。

import java.text.Collator;
import java.util.*;
public class CollatorKeys {
    public static void main(String[] args) {
        Collator collator = Collator.getInstance(Locale.US);
        collator.setStrength(Collator.PRIMARY);
        List<String> input = List.of("e", "é", "E");
        TreeSet<String> language = new TreeSet<>(collator);
        language.addAll(input);
        System.out.println("language_size=" + language.size());
        Comparator<String> precise = (a, b) -> {
            int c = collator.compare(a, b);
            return c != 0 ? c : a.compareTo(b);
        };
        TreeSet<String> exact = new TreeSet<>(precise);
        exact.addAll(input);
        System.out.println("exact_size=" + exact.size());
        List<String> display = new ArrayList<>(input);
        display.sort(collator);
        System.out.println("display_size=" + display.size());
        System.out.println("raw_equal=" + input.get(0).equals(input.get(1)));
    }
}

保存为 CollatorKeys.java,执行 java CollatorKeys.java:

language_size=1
exact_size=3
display_size=3
raw_equal=false

返回零比排在前后更重要

TreeSet 沿比较器定义的顺序定位元素。两个对象比较为零,就占据同一个集合位置,后来的 add 返回 false。它不会再额外调用一次字符串equals,替业务确认究竟要不要保留。

这就是本例里 language_size=1 的原因。输入顺序决定首先留下哪一个展示值,因此不能把剩下的那个字符串当成某种规范化结果。换一下插入顺序,可能留下另一种大小写或重音形式。

这种差异还会影响 contains、remove 以及与其他Set实现的组合。HashSet主要依据equals与hashCode,TreeSet使用比较器;当两个集合采用不同的等价关系时,比较或集合操作可能出现读者不直觉的结果。集合本身都遵守各自契约,问题是应用没有统一身份定义。

如果只需要排序,就别顺便用 Set 去重

样本的列表排序保留了全部三个输入。对于联系人、商品名或文档标题,按语言规则排序往往只是展示需求;同名内容、重音差异和大小写差异是否代表同一个对象,应由独立记录ID决定。

若确实需要一个字符串TreeSet,并且要求原始字符串精确不同就都保留,可以在Collator比较为零时再用String.compareTo打破平局。代码里的 exact 正是如此。它在当前比较器状态不变的条件下保持三种字符串,不把自然语言等价扩大成精确身份。

不过这项修正是一种明确的排序选择,不是语言学上的唯一正确答案。相同主排序级别的字符串,按UTF-16顺序排列可能不符合用户期望;用户数据有稳定ID时,也可以用ID作为最后的平局规则。关键是把平局怎样处理写进协议,而不是碰巧依赖输入顺序。

比较器不能在集合使用中途变样

Collator可以调整strength和decomposition。把元素放进TreeSet后再修改其规则,会改变“本应位于哪里”的判断,而树结构仍按旧规则建立。这个集合不会自动重新排序。若应用决定更改排序规则,应建立新比较器和新集合,重新插入并处理新产生的冲突。

相同道理也适用于比较器读取可变对象字段。对象的标签被修改以后,TreeSet里原有位置可能不再符合排序;修改前移出、修改后再加入,或直接使用不可变排序键,比寄希望于集合自我修复更可靠。

并发场景也不要把一个可变Collator到处共享后随请求改地区。每次排序应使用确定的配置实例,或在有清楚同步策略的作用域内使用。语言数据与JDK实现也可能随版本变化,依赖持久排序键时应保留版本并制定重建办法,不能把本次本机顺序当跨版本永久编号。

最终该问的是:这份业务数据为什么要去重?如果答案是同一记录ID只出现一次,就用记录身份;如果答案是给用户按语言顺序展示,就用语言排序。只有明确决定语言等价同时代表身份时,才让Collator直接控制TreeSet的合并范围。

正文到此结束
评论插件初始化中...
Loading...
本文目录