说明:以下习题围绕今日授课核心内容——比较器排序(Comparable 自然排序 / Comparator 比较器排序) 与 可变参数(Varargs) 展开,涵盖 Comparable 接口实现与
compareTo(T o)重写、Comparator 接口与匿名内部类、TreeSet/TreeMap 有序集合自动排序、返回值规则(负数存左 / 0 重复不存 / 正数存右)、Collections.sort(list, comparator)、Double.compare、compareTo 与 equals 一致性、可变参数形参类型... 参数名(本质即数组、可变参数必须放最后)等知识点。题目分为 基础练习(Comparable 自然排序 / Comparator 比较器排序 / Collections.sort 排序 / 可变参数,5 题)和 进阶挑战(比较器综合应用,2 题)。
以下练习围绕今日授课核心内容展开,由浅入深。
难度:⭐
知识点:Comparable<T> 接口、compareTo(T o) 重写、TreeSet 有序 + 去重、返回值规则
场景描述:TreeSet 是有序集合,取出的顺序是排序过的。不是所有类型都能直接存入 TreeSet——必须实现 Comparable 接口(自然排序),或在创建集合时于构造方法中传入 Comparator(比较器排序)。本节课 Student 类实现自然排序:按年龄升序,年龄相同再按姓名排序。
题目要求:
定义 Student 类(name、age、score 三个属性),实现 Comparable<Student> 接口,重写 compareTo(T o) 方法:
public class Student implements Comparable<Student> {
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;
}
}
编写测试类 NaturalSortTest:
import java.util.TreeSet;
public class NaturalSortTest {
public static void main(String[] args) {
// 1. 创建 TreeSet<Student>(不传 Comparator → 使用自然排序)
// 2. 添加 4 名学生:张三20 / 李四22 / 王五20 / 赵六21(注意:张三和王五年龄相同)
// 3. foreach 遍历输出,观察自动排序结果
// 4. 再添加一名"刘七20",观察:刘七能否存入?为什么?(提示:compareTo 返回值规则)
}
}
预期输出:
张三 20 85
王五 20 78 ← 年龄 20 与"张三"相同 → 触发姓名二次比较(王 < 张)
赵六 21 88
李四 22 90
思考题:
难度:⭐⭐
知识点:Comparator<T> 接口、compare(o1, o2) 重写、匿名内部类、TreeSet(Comparator) 构造方法
场景描述:类设计期间已经实现 Comparable(默认按年龄排序),但现在临时需要按别的规则排序(如成绩),又不修改 Student 类。此时使用比较器排序——创建集合对象时,在构造方法中传入 Comparator 接口的实现类对象(匿名内部类)。
题目要求:
import java.util.Comparator;
import java.util.TreeSet;
public class ComparatorSortTest {
public static void main(String[] args) {
// 1. 创建 TreeSet<Student>,构造方法传入匿名内部类 Comparator<Student>
// 排序规则:按成绩(score)升序,成绩相同再按姓名排序
// 提示:TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
// @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
思考题:
new TreeSet<>(new Comparator<Student>(){...}) 就能覆盖默认的自然排序?compare(o1, o2) 和 Comparable 的 compareTo(o) 写法有什么不同?(提示:一个是"两个对象在外部比较",一个是"this 与 o 比较")难度:⭐⭐
知识点:Collections.sort(list, comparator)、匿名内部类、Double.compare(a, b)、降序技巧(交换参数)
场景描述:ArrayList 是无序的,想让它按指定规则排序,用 Collections.sort(list, comparator)——传入匿名内部类 Comparator,对 List 原地排序(改变原集合顺序)。
题目要求:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
public class ListSortTest {
public static void main(String[] args) {
// 1. 创建 ArrayList<Student>,添加 4 名学生:张三20-85 / 李四22-90 / 王五19-78 / 赵六21-88
// 2. 匿名内部类 Comparator:按年龄升序排序(s1.getAge() - s2.getAge())
// Collections.sort(list, new Comparator<Student>() {...});
// 遍历输出
// 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
思考题:
(int)(s1.getScore() - s2.getScore())?用 Double.compare 有什么好处?(提示:浮点精度 + 强转截断)Collections.sort(list, comparator) 和 TreeSet 自动排序的区别是什么?(提示:一个改变已有 List 的顺序,一个在存入时排序;TreeSet 还会去重)难度:⭐⭐
知识点:可变参数 int... a、for each 遍历、方法重载 vs 可变参数、可变参数本质即数组
场景描述:以前计算"多个整数相加",有几个参数就要定义几个方法重载(add(int a, int b) / add(int a, int b, int c)...),写多少个都覆盖不完。有了可变参数,一个方法就能接收任意个数的参数——方便用户使用,也方便自己定义。
题目要求:
定义 Calculator 类,编写可变参数方法 add(int... a),用 for each 累加求和:
public class Calculator {
// TODO: 编写可变参数方法 add(int... a),计算任意个数整数的和
// 提示:int sum = 0;
// for (int n : a) { sum += n; }
// return sum;
public int add(int... a) {
return 0; // 请补全
}
public static void main(String[] args) {
Calculator calc = new Calculator();
// 1. 同一个方法,传入不同个数的参数:
// add(1, 2) → 3
// add(1, 2, 3) → 6
// add(1, 2, 3, 4, 5) → 15
// add(1,2,...,10) → 55
// 2. 验证"可变参数本质就是数组":用数组调用,效果与直接传参数一致
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// calc.add(arr) → 55
}
}
预期输出:
add(1, 2) = 3
add(1, 2, 3) = 6
add(1, 2, 3, 4, 5) = 15
add(1~10 累加) = 55
用数组调用 add(arr) = 55
思考题:
int... a 和 int[] a 是什么关系?为什么两者调用效果一样?(提示:可变参数是数组的语法糖)add(int... a) 改成 add(int[] a),调用时还能写 add(1, 2, 3) 吗?(提示:不能,数组参数必须显式传数组)难度:⭐⭐ 知识点:固定参数与可变参数共存、可变参数必须放在最后、求总分/平均分/最大值、可变参数的综合应用
场景描述:一个学生有多门课程成绩,成绩个数不定。要求:方法同时接收"学生姓名"(固定参数)和"若干门成绩"(可变参数),统计输出总分和平均分。注意:可变参数必须放在参数列表的最后。
题目要求:
定义 ScoreCalculator 类,编写方法 showScore(String name, int... scores):
public class ScoreCalculator {
// TODO: 编写方法 showScore(String name, int... scores)
// 1) 输出学生姓名
// 2) 遍历 scores 求和(total)和求最大值(max)
// 3) 输出各科成绩、总分、平均分(平均分用 double,total * 1.0 / scores.length)
public static void main(String[] args) {
ScoreCalculator sc = new ScoreCalculator();
sc.showScore("张三", 85, 90, 78); // 3 门课
sc.showScore("李四", 92, 88, 95, 80, 90); // 5 门课
}
}
进阶挑战:再实现两个可变参数方法,体会通用性:
max(int... a):返回任意个数整数中的最大值join(String separator, String... strings):用分隔符拼接若干字符串,如 join("-", "Java", "SE", "260713") → "Java-SE-260713"(注意:固定参数 separator 在前,可变参数在后)预期输出:
========== 张三的成绩单 ==========
姓名:张三
各科成绩:85, 90, 78
总分:253,平均分:84.33
最高分:90
========== 李四的成绩单 ==========
姓名:李四
各科成绩:92, 88, 95, 80, 90
总分:445,平均分:89.0
最高分:95
========== 可变参数进阶 ==========
max(3, 9, 5, 1) = 9
join("-", "Java", "SE", "260713") = Java-SE-260713
思考题:
showScore(String name, int... scores) 中,可变参数能写在 name 前面吗?为什么?(提示:可变参数必须放在参数列表最后,否则编译报错)showScore("张三")(不传成绩),scores.length 是多少?会不会报错?(提示:可变参数可传 0 个,scores 是长度为 0 的数组)join 方法为什么把分隔符 separator 放在前面、可变参数放后面?如果反过来 join(String... strings, String separator) 行吗?(提示:违背"可变参数置末"规则)以下练习综合运用 Comparable / Comparator、TreeMap、compareTo 与 equals 一致性等知识。
难度:⭐⭐⭐
知识点:TreeMap<K,V> 按键排序、自定义类型作为 Key、entrySet() 遍历、compareTo/equals 一致性
场景描述:TreeMap 是按键排序的有序 Map。把 Student 对象作为 Key、班级作为 Value 存入 TreeMap。因为 Student 实现了 Comparable,TreeMap 会自动按 compareTo 的规则对键排序——实现"成绩排行榜"。
题目要求:
定义 Student 类(实现 Comparable,按成绩降序排序,成绩相同按姓名),修改 compareTo:
public class Student implements Comparable<Student> {
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;
}
}
编写测试类 ScoreRankingTest:
import java.util.Map;
import java.util.TreeMap;
public class ScoreRankingTest {
public static void main(String[] args) {
// 1. 创建 TreeMap<Student, String>:学生 → 所在班级
// 提示:Map<Student, String> map = new TreeMap<>();
// 添加:张三90-"一班"、李四85-"二班"、王五95-"三班"、赵六88-"一班"
// 2. entrySet() 遍历输出,观察是否按成绩从高到低排序
// 输出格式:"王五 95 - 三班"
// 3. 思考:如果两个学生成绩相同且姓名相同(属性完全一致),再 put 会发生什么?
// —— compareTo 返回 0 → 视为同一个键 → 覆盖旧值(去重)
}
}
预期输出:
王五 95 - 三班
张三 90 - 一班
赵六 88 - 一班
李四 85 - 二班
原理分析(必答):
compareTo 方法,返回 0 即视为重复new TreeMap<Student, String>() 会怎样?(提示:存入第一个键时抛 ClassCastException)难度:⭐⭐⭐⭐
知识点:Comparable 与 Comparator 结合、hashCode() / equals() 重写、compareTo 与 equals 一致性、多维度排序、迭代器遍历
场景描述:某公司开发员工信息管理系统。员工类 Employee(工号、姓名、薪资、部门)。要求:
题目要求:
定义 Employee 类:
import java.util.Objects;
public class Employee implements Comparable<Employee> {
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() { ... }
}
编写测试类 EmployeeSystem:
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
public class EmployeeSystem {
public static void main(String[] args) {
// 1. 创建 TreeSet<Employee>(自然排序)
// 添加 5 名员工(其中两名工号相同,测试去重):
// E001-张三-12000-技术部 / E002-李四-15000-市场部
// E003-王五-15000-技术部 / E004-赵六-9000-财务部
// E005-刘七-12000-市场部
// 2. 迭代器遍历输出 → 观察:按薪资降序,薪资相同按工号;工号重复的自动去重
// 输出格式:"E001-张三-12000.0-技术部"
// 3. 临时需求:通讯录按姓名排序 —— 创建 TreeSet<Employee> 传入按姓名排序的 Comparator
// 匿名内部类:o1.getName().compareTo(o2.getName())
// 迭代器遍历输出
// 4. 思考:E001-张三(12000) 和 E005-刘七(12000) 薪资相同,为何都能存入?
// —— compareTo 在薪资相同后继续比较工号,返回值不为 0
}
}
预期输出:
========== 默认排序:按薪资降序(薪资相同按工号) ==========
E002-李四-15000.0-市场部
E003-王五-15000.0-技术部 ← 与李四薪资相同 → 按工号 E002<E003
E001-张三-12000.0-技术部
E005-刘七-12000.0-市场部 ← 与张三薪资相同 → 按工号 E001<E005
E004-赵六-9000.0-财务部
========== 通讯录:按姓名排序 ==========
E002-李四-15000.0-市场部
E005-刘七-12000.0-市场部
E003-王五-15000.0-技术部
E001-张三-12000.0-技术部
E004-赵六-9000.0-财务部
原理分析(必答):
E002-李四 和 E003-王五 薪资相同(都是 15000)却能都存入 TreeSet?—— compareTo 返回 0 才算重复,薪资相同后还比较了工号,返回值不为 0public class Student implements Comparable<Student> {
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<Student> 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 岁两人按姓名排序)
TreeSet<Student> set = new TreeSet<>(new Comparator<Student>() {
@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(成绩升序,与年龄无关)
ArrayList<Student> 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<Student>() {
@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<Student>() {
@Override
public int compare(Student s1, Student s2) {
return Double.compare(s2.getScore(), s1.getScore());
}
});
for (Student s : list) {
System.out.println(s);
}
public class Calculator {
// 可变参数:本质就是 int[] a
public int add(int... a) {
int sum = 0;
for (int n : a) {
sum += n;
}
return sum;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println("add(1, 2) = " + calc.add(1, 2));
System.out.println("add(1, 2, 3) = " + calc.add(1, 2, 3));
System.out.println("add(1, 2, 3, 4, 5) = " + calc.add(1, 2, 3, 4, 5));
int sum10 = calc.add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
System.out.println("add(1~10 累加) = " + sum10);
// 可变参数本质就是数组:用数组调用效果一致
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println("用数组调用 add(arr) = " + calc.add(arr));
}
}
public class ScoreCalculator {
// 固定参数 name + 可变参数 scores(可变参数必须放最后)
public void showScore(String name, int... scores) {
System.out.println("========== " + name + "的成绩单 ==========");
System.out.println("姓名:" + name);
int total = 0;
int max = scores[0]; // 假设至少有一门成绩
StringBuilder sb = new StringBuilder();
for (int i = 0; i < scores.length; i++) {
total += scores[i];
if (scores[i] > max) {
max = scores[i];
}
sb.append(scores[i]).append(i == scores.length - 1 ? "" : ", ");
}
System.out.println("各科成绩:" + sb);
System.out.println("总分:" + total + ",平均分:" + String.format("%.2f", total * 1.0 / scores.length));
System.out.println("最高分:" + max);
}
// 进阶:求任意个数整数的最大值
public int max(int... a) {
int max = a[0];
for (int n : a) {
if (n > max) max = n;
}
return max;
}
// 进阶:用分隔符拼接若干字符串(固定参数在前,可变参数在后)
public String join(String separator, String... strings) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < strings.length; i++) {
sb.append(strings[i]);
if (i != strings.length - 1) {
sb.append(separator);
}
}
return sb.toString();
}
public static void main(String[] args) {
ScoreCalculator sc = new ScoreCalculator();
sc.showScore("张三", 85, 90, 78);
sc.showScore("李四", 92, 88, 95, 80, 90);
System.out.println("========== 可变参数进阶 ==========");
System.out.println("max(3, 9, 5, 1) = " + sc.max(3, 9, 5, 1));
System.out.println("join(\"-\", \"Java\", \"SE\", \"260713\") = " + sc.join("-", "Java", "SE", "260713"));
}
}
// 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<Student, String> 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<Student, String> me : map.entrySet()) {
System.out.println(me.getKey().getName() + " " + me.getKey().getScore() + " - " + me.getValue());
}
// 输出顺序:王五95 → 张三90 → 赵六88 → 李四85(成绩降序)
// 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<Employee> 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<Employee> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
// 输出:E002 → E003 → E001 → E005 → E004(薪资降序,同薪按工号)
// 通讯录:按姓名排序的临时 Comparator
TreeSet<Employee> byName = new TreeSet<>(new Comparator<Employee>() {
@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 没有索引,用迭代器统一遍历 |
| 可变参数定义与调用 | 练习 4、练习 5 | int... a 参数个数可变,一个方法接收任意个数参数 |
| 可变参数本质即数组 | 练习 4 | int... a 等价于 int[] a,可用数组直接调用 |
| 方法重载 vs 可变参数 | 练习 4 | 参数个数不确定用可变参数;个数固定、类型不同用重载 |
| 固定参数 + 可变参数共存 | 练习 5 | 可变参数必须放在参数列表最后,否则编译报错 |
| 可变参数综合应用 | 练习 5 | 求总分/平均分/最大值、字符串拼接 join |