# 2026-08-03 比较器排序专项练习(课堂练习用) > **说明**:以下习题围绕今日授课核心内容——**比较器排序(Comparable 自然排序 / Comparator 比较器排序)** 展开,涵盖 Comparable 接口实现与 `compareTo(T o)` 重写、Comparator 接口与匿名内部类、TreeSet/TreeMap 有序集合自动排序、返回值规则(负数存左 / 0 重复不存 / 正数存右)、`Collections.sort(list, comparator)`、`Double.compare`、compareTo 与 equals 一致性等知识点。 > > 题目分为 **基础练习**(Comparable 自然排序 / Comparator 比较器排序 / Collections.sort 排序,3 题)和 **进阶挑战**(比较器综合应用,2 题)。 --- # 第一部分:基础练习 > 以下练习围绕今日授课核心内容展开,由浅入深。 --- ## 练习 1:Comparable 自然排序(TreeSet 自动排序 + 去重) **难度**:⭐ **知识点**:`Comparable` 接口、`compareTo(T o)` 重写、`TreeSet` 有序 + 去重、返回值规则 **场景描述**:TreeSet 是有序集合,取出的顺序是**排序过**的。不是所有类型都能直接存入 TreeSet——必须实现 `Comparable` 接口(自然排序),或在创建集合时于构造方法中传入 `Comparator`(比较器排序)。本节课 Student 类实现自然排序:**按年龄升序,年龄相同再按姓名排序**。 **题目要求**: 1. 定义 `Student` 类(`name`、`age`、`score` 三个属性),**实现 `Comparable` 接口**,重写 `compareTo(T o)` 方法: ```java public class Student implements Comparable { private String name; private int age; private double score; public Student(String name, int age, double score) { this.name = name; this.age = age; this.score = score; } // TODO: 补全 getter / setter / toString 方法 // TODO: 重写 compareTo —— 按年龄升序,年龄相同再按姓名 @Override public int compareTo(Student o) { // 提示:int result = this.age - o.getAge(); // return result == 0 ? this.name.compareTo(o.getName()) : result; } } ``` 2. 编写测试类 `NaturalSortTest`: ```java import java.util.TreeSet; public class NaturalSortTest { public static void main(String[] args) { // 1. 创建 TreeSet(不传 Comparator → 使用自然排序) // 2. 添加 4 名学生:张三20 / 李四22 / 王五20 / 赵六21(注意:张三和王五年龄相同) // 3. foreach 遍历输出,观察自动排序结果 // 4. 再添加一名"刘七20",观察:刘七能否存入?为什么?(提示:compareTo 返回值规则) } } ``` **预期输出**: ``` 张三 20 85 王五 20 78 ← 年龄 20 与"张三"相同 → 触发姓名二次比较(王 < 张) 赵六 21 88 李四 22 90 ``` **思考题**: 1. 返回值为负数 / 0 / 正数分别代表什么?元素会存入左边、视为重复不存、还是存入右边? 2. 为什么年龄相同的"张三"和"王五"都能存入?—— compareTo 里做了**二次比较**(姓名),返回值不为 0,所以不视为重复 3. 如果 compareTo 只按年龄比较,添加"刘七20"会发生什么?(提示:20 与已有节点比较返回 0 → 视为重复不存) --- ## 练习 2:Comparator 比较器排序(TreeSet 构造传入匿名内部类) **难度**:⭐⭐ **知识点**:`Comparator` 接口、`compare(o1, o2)` 重写、匿名内部类、`TreeSet(Comparator)` 构造方法 **场景描述**:类设计期间已经实现 Comparable(默认按年龄排序),但现在**临时**需要按别的规则排序(如成绩),又**不修改** Student 类。此时使用**比较器排序**——创建集合对象时,在构造方法中传入 `Comparator` 接口的实现类对象(匿名内部类)。 **题目要求**: ```java import java.util.Comparator; import java.util.TreeSet; public class ComparatorSortTest { public static void main(String[] args) { // 1. 创建 TreeSet,构造方法传入匿名内部类 Comparator // 排序规则:按成绩(score)升序,成绩相同再按姓名排序 // 提示:TreeSet set = new TreeSet<>(new Comparator() { // @Override // public int compare(Student o1, Student o2) { // int result = (int)(o1.getScore() - o2.getScore()); // return result == 0 ? o1.getName().compareTo(o2.getName()) : result; // } // }); // 2. 添加 4 名学生:张三85 / 李四90 / 王五78 / 赵六88 // 3. foreach 遍历输出,观察:这次是按【成绩】排序,而不是年龄 // 4. 与练习 1 的输出顺序对比,体会"自然排序 vs 比较器排序" } } ``` **预期输出**: ``` 王五 20 78 张三 7 85 赵六 222 88 李四 2 90 ``` **思考题**: 1. 为什么 `new TreeSet<>(new Comparator(){...})` 就能覆盖默认的自然排序? 2. Comparator 的 `compare(o1, o2)` 和 Comparable 的 `compareTo(o)` 写法有什么不同?(提示:一个是"两个对象在外部比较",一个是"this 与 o 比较") 3. 匿名内部类为什么适合用来传 Comparator?(提示:临时规则、用完即弃、无需新建类文件) --- ## 练习 3:Collections.sort 对 List 排序(按年龄升序 / 按成绩降序) **难度**:⭐⭐ **知识点**:`Collections.sort(list, comparator)`、匿名内部类、`Double.compare(a, b)`、降序技巧(交换参数) **场景描述**:`ArrayList` 是**无序**的,想让它按指定规则排序,用 `Collections.sort(list, comparator)`——传入匿名内部类 Comparator,对 List **原地排序**(改变原集合顺序)。 **题目要求**: ```java import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; public class ListSortTest { public static void main(String[] args) { // 1. 创建 ArrayList,添加 4 名学生:张三20-85 / 李四22-90 / 王五19-78 / 赵六21-88 // 2. 匿名内部类 Comparator:按年龄升序排序(s1.getAge() - s2.getAge()) // Collections.sort(list, new Comparator() {...}); // 遍历输出 // 3. 匿名内部类 Comparator:按成绩(double)降序排序 // 注意:成绩是 double,不能直接相减强转 int // 提示:Double.compare(s2.getScore(), s1.getScore()) ← 注意参数顺序 = 降序 // 遍历输出 } } ``` **预期输出**: ``` ========== 按年龄升序 ========== 王五 19 78 张三 20 85 赵六 21 88 李四 22 90 ========== 按成绩降序 ========== 李四 22 90 赵六 21 88 张三 20 85 王五 19 78 ``` **思考题**: 1. double 类型的成绩为什么不能用 `(int)(s1.getScore() - s2.getScore())`?用 `Double.compare` 有什么好处?(提示:浮点精度 + 强转截断) 2. `Collections.sort(list, comparator)` 和 `TreeSet` 自动排序的区别是什么?(提示:一个改变已有 List 的顺序,一个在存入时排序;TreeSet 还会去重) 3. 想让 Comparator 排序方向反过来,最简单的做法是什么?(提示:交换 compare 中两个参数的位置) --- # 第二部分:进阶挑战 > 以下练习综合运用 Comparable / Comparator、TreeMap、compareTo 与 equals 一致性等知识。 --- ## 进阶 1:成绩排行榜(TreeMap + Comparable 键排序) **难度**:⭐⭐⭐ **知识点**:`TreeMap` 按键排序、自定义类型作为 Key、`entrySet()` 遍历、compareTo/equals 一致性 **场景描述**:TreeMap 是**按键排序**的有序 Map。把 `Student` 对象作为 **Key**、班级作为 Value 存入 TreeMap。因为 Student 实现了 Comparable,TreeMap 会自动按 compareTo 的规则对键排序——实现"成绩排行榜"。 **题目要求**: 1. 定义 `Student` 类(实现 Comparable,**按成绩降序**排序,成绩相同按姓名),修改 compareTo: ```java public class Student implements Comparable { private String name; private int age; private double score; // 构造方法、getter/setter、toString(略) @Override public int compareTo(Student o) { // TODO: 按成绩降序(注意:谁减谁决定升降序) // 提示:int result = Double.compare(o.getScore(), this.score); // 降序 // return result == 0 ? this.name.compareTo(o.getName()) : result; } } ``` 2. 编写测试类 `ScoreRankingTest`: ```java import java.util.Map; import java.util.TreeMap; public class ScoreRankingTest { public static void main(String[] args) { // 1. 创建 TreeMap:学生 → 所在班级 // 提示:Map map = new TreeMap<>(); // 添加:张三90-"一班"、李四85-"二班"、王五95-"三班"、赵六88-"一班" // 2. entrySet() 遍历输出,观察是否按成绩从高到低排序 // 输出格式:"王五 95 - 三班" // 3. 思考:如果两个学生成绩相同且姓名相同(属性完全一致),再 put 会发生什么? // —— compareTo 返回 0 → 视为同一个键 → 覆盖旧值(去重) } } ``` **预期输出**: ``` 王五 95 - 三班 张三 90 - 一班 赵六 88 - 一班 李四 85 - 二班 ``` **原理分析(必答)**: 1. TreeMap 是怎么判断两个键是否重复的?—— 调用键的 `compareTo` 方法,返回 0 即视为重复 2. 官方为什么强烈推荐"compareTo == 0 时 equals 返回 true、hashCode 一致"?—— 保证排序判定与相等判定保持一致,避免集合去重行为出现矛盾 3. 如果 Student 没有实现 Comparable 也不传 Comparator,`new TreeMap()` 会怎样?(提示:存入第一个键时抛 ClassCastException) --- ## 进阶 2:员工信息管理系统(多排序规则 + compareTo/equals 一致性 + 综合应用) **难度**:⭐⭐⭐⭐ **知识点**:Comparable 与 Comparator 结合、`hashCode()` / `equals()` 重写、compareTo 与 equals 一致性、多维度排序、迭代器遍历 **场景描述**:某公司开发员工信息管理系统。员工类 `Employee`(工号、姓名、薪资、部门)。要求: - **默认排序**(自然排序):按薪资降序,薪资相同再按工号 - 员工加入 TreeSet 后**自动排序**且**自动去重**(工号相同的员工视为同一人) - 临时需求:按姓名排序输出一份通讯录(使用 Comparator 比较器排序,不修改 Employee 类) **题目要求**: 1. 定义 `Employee` 类: ```java import java.util.Objects; public class Employee implements Comparable { private String id; // 工号 private String name; // 姓名 private double salary; // 薪资 private String dept; // 部门 public Employee(String id, String name, double salary, String dept) { this.id = id; this.name = name; this.salary = salary; this.dept = dept; } // TODO: 补全 getter / setter / toString 方法 // TODO: 重写 compareTo —— 按薪资降序,薪资相同再按工号 @Override public int compareTo(Employee o) { // 提示:Double.compare(o.getSalary(), this.salary) // 降序 // 若返回 0,再比较工号:this.id.compareTo(o.getId()) } // TODO: 重写 equals / hashCode —— 与 compareTo 保持一致(工号相同即视为同一人) @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } } ``` 2. 编写测试类 `EmployeeSystem`: ```java import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; public class EmployeeSystem { public static void main(String[] args) { // 1. 创建 TreeSet(自然排序) // 添加 5 名员工(其中两名工号相同,测试去重): // E001-张三-12000-技术部 / E002-李四-15000-市场部 // E003-王五-15000-技术部 / E004-赵六-9000-财务部 // E005-刘七-12000-市场部 // 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重 // 输出格式:"E001-张三-12000.0-技术部" // 3. 临时需求:通讯录按姓名排序 —— 创建 TreeSet 传入按姓名排序的 Comparator // 匿名内部类:o1.getName().compareTo(o2.getName()) // 迭代器遍历输出 // 4. 思考:E001-张三(12000) 和 E005-刘七(12000) 薪资相同,为何都能存入? // —— compareTo 在薪资相同后继续比较工号,返回值不为 0 } } ``` **预期输出**: ``` ========== 默认排序:按薪资降序(薪资相同按工号) ========== E002-李四-15000.0-市场部 E003-王五-15000.0-技术部 ← 与李四薪资相同 → 按工号 E002 { private String name; private int age; private double score; // 构造方法、getter/setter、toString(略) @Override public int compareTo(Student o) { // 按年龄升序 int result = this.age - o.getAge(); // 年龄相同 → 按姓名升序(二次比较) return result == 0 ? this.name.compareTo(o.getName()) : result; } } // NaturalSortTest TreeSet set = new TreeSet<>(); set.add(new Student("张三", 20, 85)); set.add(new Student("李四", 22, 90)); set.add(new Student("王五", 20, 78)); set.add(new Student("赵六", 21, 88)); for (Student s : set) { System.out.println(s); } // 输出顺序:王五20 → 张三20 → 赵六21 → 李四22(年龄升序,20 岁两人按姓名排序) ``` ## 练习 2 参考 ```java TreeSet set = new TreeSet<>(new Comparator() { @Override public int compare(Student o1, Student o2) { // 按成绩升序,成绩相同再按姓名 int result = (int) (o1.getScore() - o2.getScore()); return result == 0 ? o1.getName().compareTo(o2.getName()) : result; } }); set.add(new Student("张三", 7, 85)); set.add(new Student("李四", 2, 90)); set.add(new Student("王五", 20, 78)); set.add(new Student("赵六", 222, 88)); for (Student s : set) { System.out.println(s); } // 输出顺序:王五78 → 张三85 → 赵六88 → 李四90(成绩升序,与年龄无关) ``` ## 练习 3 参考 ```java ArrayList list = new ArrayList<>(); list.add(new Student("张三", 20, 85)); list.add(new Student("李四", 22, 90)); list.add(new Student("王五", 19, 78)); list.add(new Student("赵六", 21, 88)); // 按年龄升序 Collections.sort(list, new Comparator() { @Override public int compare(Student s1, Student s2) { return s1.getAge() - s2.getAge(); } }); for (Student s : list) { System.out.println(s); } // 按成绩降序(Double.compare + 交换参数 = 降序) Collections.sort(list, new Comparator() { @Override public int compare(Student s1, Student s2) { return Double.compare(s2.getScore(), s1.getScore()); } }); for (Student s : list) { System.out.println(s); } ``` ## 进阶 1 参考 ```java // Student.compareTo:按成绩降序,成绩相同按姓名 @Override public int compareTo(Student o) { int result = Double.compare(o.getScore(), this.score); // 降序 return result == 0 ? this.name.compareTo(o.getName()) : result; } // ScoreRankingTest TreeMap map = new TreeMap<>(); map.put(new Student("张三", 18, 90), "一班"); map.put(new Student("李四", 19, 85), "二班"); map.put(new Student("王五", 20, 95), "三班"); map.put(new Student("赵六", 18, 88), "一班"); for (Map.Entry me : map.entrySet()) { System.out.println(me.getKey().getName() + " " + me.getKey().getScore() + " - " + me.getValue()); } // 输出顺序:王五95 → 张三90 → 赵六88 → 李四85(成绩降序) ``` ## 进阶 2 参考(核心逻辑) ```java // Employee.compareTo:按薪资降序,薪资相同按工号 @Override public int compareTo(Employee o) { int result = Double.compare(o.getSalary(), this.salary); // 薪资降序 return result == 0 ? this.id.compareTo(o.getId()) : result; } // equals / hashCode:与 compareTo 一致,以工号判定 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return Objects.equals(id, employee.id); } @Override public int hashCode() { return Objects.hash(id); } // EmployeeSystem TreeSet set = new TreeSet<>(); set.add(new Employee("E001", "张三", 12000, "技术部")); set.add(new Employee("E002", "李四", 15000, "市场部")); set.add(new Employee("E003", "王五", 15000, "技术部")); set.add(new Employee("E004", "赵六", 9000, "财务部")); set.add(new Employee("E005", "刘七", 12000, "市场部")); Iterator it = set.iterator(); while (it.hasNext()) { System.out.println(it.next()); } // 输出:E002 → E003 → E001 → E005 → E004(薪资降序,同薪按工号) // 通讯录:按姓名排序的临时 Comparator TreeSet byName = new TreeSet<>(new Comparator() { @Override public int compare(Employee o1, Employee o2) { return o1.getName().compareTo(o2.getName()); } }); byName.addAll(set); for (Employee e : byName) { System.out.println(e); } ``` --- # 涵盖知识点总览 | 知识点 | 对应练习 | 说明 | |--------|---------|------| | Comparable 接口与 compareTo 重写 | 练习 1、进阶 1、进阶 2 | 实体类内建默认排序规则(自然排序) | | 返回值规则(负数/0/正数) | 练习 1、练习 2 | 负数存左、0 重复不存、正数存右 | | 二次比较(多字段排序) | 练习 1、进阶 1、进阶 2 | 主排序相同 → 叠加次排序字段,避免误判重复 | | Comparator 接口与匿名内部类 | 练习 2、练习 3 | 临时自定义排序,不改实体类 | | TreeSet(Comparator) 构造方法 | 练习 2 | 构造方法传入比较器 → 覆盖自然排序 | | Collections.sort(list, comparator) | 练习 3 | 对 List 原地排序 | | Double.compare(a, b) | 练习 3、进阶 1、进阶 2 | double 比较的标准做法,避免精度问题 | | 降序技巧(交换参数) | 练习 3、进阶 2 | compare(s2, s1) / compareTo 中交换比较顺序 | | TreeMap 按键排序 | 进阶 1 | 自定义类型作为 Key,按 compareTo 排序并去重 | | compareTo 与 equals/hashCode 一致性 | 进阶 1、进阶 2 | compareTo == 0 → equals 返回 true、hashCode 一致 | | TreeSet 自动排序 + 去重 | 练习 1、进阶 2 | compareTo 返回 0 视为重复,不存入 | | Iterator 迭代器遍历 | 进阶 2 | Set 没有索引,用迭代器统一遍历 |