treeset 默认按元素自然顺序升序排列,调用 compareto() 判断大小和去重;要求元素实现 comparable 接口,否则抛 classcastexception;字符串按 unicode 码点逐字符比较。

TreeSet 默认怎么排序?
TreeSet 默认按元素的自然顺序升序排列,前提是元素类型实现了 Comparable 接口——比如 Integer、String、LocalDateTime 都自带这个能力。它内部调用的是每个元素的 compareTo() 方法,不是靠 equals() 判断重复,而是靠比较结果为 0 才视为“相等”并拒绝插入。
常见错误现象:
- 往空 TreeSet<student></student> 里 add 对象直接抛 ClassCastException 或 NullPointerException(JDK8+);
- 表面加进去了,但两个“内容相同”的对象没去重——因为没重写 compareTo(),默认按内存地址比,永远不等。
- 字符串默认按 Unicode 编码逐字符比较,
"apple""banana",但"Apple">"banana"(大写 A 的码点比小写 b 小?错,A=65,b=98,所以"Apple""banana";真正坑是大小写混排时顺序不符合直觉) - 数值类型如
Integer比较的是实际值,不是 hash 值或引用 - JDK8 起明确禁止往
TreeSet中插入null,否则在首次比较时就炸:NullPointerException
让自定义类支持 TreeSet 自然排序
必须让类实现 Comparable 接口,并重写 compareTo()。这不是可选项,是硬性前提。
实操建议:
- 方法签名必须是 public int compareTo(T o),泛型类型要一致(别写 Object 后强转,易出 ClassCastException);
- 返回负数表示“当前对象小”,正数表示“当前对象大”,0 表示“逻辑相等”(TreeSet 会据此去重);
- 多字段排序时,用 Integer.compare(this.age, o.age) 这类静态工具方法,避免手动减法溢出(比如 age1 - age2 可能整型溢出)。
public class Student implements Comparable<Student> {
private String name;
private int age;
<pre class='brush:java;toolbar:false;'>@Override
public int compareTo(Student o) {
int nameCmp = this.name.compareTo(o.name);
if (nameCmp != 0) return nameCmp;
return Integer.compare(this.age, o.age); // 升序
}}
- 如果
compareTo()总返回 0,所有对象都被当成同一个,TreeSet 只存一个 - 如果逻辑上“相等”但字段有细微差异(如浮点数精度),务必定义清晰的相等边界,否则去重行为不可控
- 实现
Comparable后,该类在其他场景(如Collections.sort()、Arrays.sort())也能复用排序逻辑
不用改类,临时指定排序规则:用 Comparator
当你无法修改目标类(比如第三方库的类),或同一类需多种排序方式(按年龄、按姓名、按分数倒序),就用 Comparator。它和 Comparable 是正交方案,互不干扰。
实操建议:
- 构造 TreeSet 时传入 Comparator 实例,例如 new TreeSet(Comparator.comparing(Student::getAge));
- 使用 Comparator.reversed() 快速取反,比手写 o2 - o1 更安全;
- 链式调用支持多级排序:Comparator.comparing(Student::getAge).thenComparing(Student::getName)。
TreeSet<Student> set = new TreeSet<>(
Comparator.comparing(Student::getAge)
.thenComparing(Student::getName)
.reversed() // 先按年龄升序,同龄再按姓名升序,最后整体倒序
);- 匿名内部类写法(老项目可能还在用)容易漏写泛型,导致
compare(Object, Object)强转失败 - Lambda 表达式里别捕获可变变量,否则排序过程中变量变了,行为不可预测
- 如果
Comparator里抛异常(比如字段为 null 且没判空),TreeSet 的 add/remove 都会中断并抛出
Comparator 和 Comparable 冲突吗?优先级怎么算?
不冲突,而且有明确优先级:**只要构造 TreeSet 时传了 Comparator,就完全忽略元素自身的 Comparable 实现**。TreeSet 只认自己持有的那个比较器。
这意味着:
- 同一个 Student 类既实现了 Comparable(按姓名),又被放进 new TreeSet(Comparator.comparing(Student::getAge)),那它就只按年龄排;
- 如果你误传了 null 给构造器(如 new TreeSet((Comparator)null)),运行时会 NPE,不是编译报错。
- 没有显式传
Comparator时,TreeSet 才去找元素的compareTo();找不到就炸 - 调试时发现排序不对,第一反应不是看类有没有实现
Comparable,而是检查构造 TreeSet 时有没有传参、传的是不是你想要的那个比较器 - 序列化/反序列化 TreeSet 时,
Comparator必须也实现Serializable,否则反序列化失败;Lambda 表达式默认不序列化,要用方法引用或静态比较器实例
最常被忽略的一点:TreeSet 的“有序”是插入时动态维护的,不是遍历时才排序。所以哪怕你用 add() 乱序插入,迭代出来的永远是排好序的——但如果你中途修改了对象的参与比较的字段(比如把已加入 TreeSet 的 Student 的 age 改了),TreeSet 不会重新定位,结构就坏了,后续操作可能出错或漏数据。










