20260803-笔记.md 20 KB

20260803 课堂笔记 — 比较器排序(Comparable 自然排序 / Comparator 比较器排序)+ 20260731 课后作业解析

  • 日期:2026-08-03
  • 项目:c260803
  • 包路径:course / homework0731
  • 作者:WanJL

目录

  1. 比较器概述:TreeSet / TreeMap 与两种排序方式
  2. 自然排序 Comparable(实体类内建排序规则)
  3. 比较器排序 Comparator(临时自定义排序)
  4. 返回值规则与 compareTo/equals 一致性的官方推荐
  5. 20260731 课后作业解析:Map 集合基本操作与遍历
  6. 20260731 课后作业解析:异常处理综合(自定义异常)
  7. 20260731 课后作业解析:匿名内部类 Comparator 排序
  8. 随堂练习要点
  9. 拓展阅读

1. 比较器概述:TreeSet / TreeMap 与两种排序方式

概念

之前学过的 HashMap / HashSet 是无序集合——取出的顺序和存入的顺序不一致(底层根据 hashCode 计算存储位置)。

而 TreeMap / TreeSet 是有序集合——取出的顺序和存入的顺序一致(注意:其实也不一定完全一致,而是排序过的)。

⚠️ 不是所有类型(包括自定义类)都能直接存入 TreeSet 或 TreeMap,是有条件的:

  1. 该类型实现了 Comparable 接口并重写了 compareTo(T o) 方法(自然排序);
  2. 或者在创建集合对象时,在构造方法中传入比较器对象 Comparator,进行临时的自定义排序(比较器排序)。

两种排序方式的定位

排序方式 实现方式 使用场景
自然排序(Comparable) 实体类实现 Comparable 接口,重写 compareTo(T o) 默认排序方式,类设计期间就确定好的排序规则
比较器排序(Comparator) 创建集合对象时传入 Comparator 接口实现类,重写 compare() 类定义期间没实现 Comparable,或临时需要其他方式排序

使用原则:一般还是以实现 Comparable 接口为主;只有在类定义期间没有实现 Comparable 接口,或者加入集合时需要临时做其他方式的排序,才会使用 Comparator 进行临时自定义排序。

一个完整实体类的结构回顾

// 来源:course/Demo01.java(注释部分)
public class 类名 implements Comparable<类名>{
    private 类型 属性1;
    private 类型 属性2;
    ...
    private 类型 属性n;

    无参构造方法
    有参构造方法(参数...){ }

    // 可选:内部类、代码块...

    getter方法...
    setter方法...
    equals()方法...
    hashCode()方法...
    toString()方法...
    compareTo(T o)方法...
}

2. 自然排序 Comparable(实体类内建排序规则)

概念

自然排序:实体类实现 Comparable 接口,重写 compareTo(T o) 方法,根据返回值决定排序规则。这是类的默认排序方式。

// 来源:course/Student.java
package course;

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;
    }

    // getter / setter ...(略)
    // toString() ...(略)

    @Override
    public int compareTo(Student o) {
        // 设置按照年龄排序(从小到大)
        int result = this.age - o.getAge();
        // 年龄相同 → 再按姓名排序(二次比较,保证唯一性)
        return result == 0 ? this.name.compareTo(o.getName()) : result;
    }
}

关键理解

  1. 比较的就是「当前对象 this」与「传入对象 o」:this.age - o.getAge() 正负决定了当前对象比传入对象大还是小。
  2. 二次比较:只用年龄排序时,若两名学生年龄相同(返回值 0),后一个会因「重复」而无法存入 TreeSet。所以排序依据最好唯一,或再叠加其他属性比较(如姓名)。

3. 比较器排序 Comparator(临时自定义排序)

概念

比较器排序:创建集合对象的时候,传入 Comparator 接口的实现类对象,重写 compare() 方法,根据返回值进行排序。不会修改实体类本身,是临时的、自定义的排序规则。

通常配合匿名内部类使用,实现 Comparator<Student> 并重写 compare(Student o1, Student o2)。

// 来源:course/Demo01.java(部分)
package course;

import java.util.Comparator;
import java.util.TreeSet;

public class Demo01 {
    public static void main(String[] args) {
        // 按照自定义的要求临时进行排序:先按成绩升序,成绩相同再按姓名排序
        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);
        }
    }
}
// 来源:course/Demo01.java(部分)
// 不传 Comparator → 使用 Student 类自带的自然排序(Comparable:按年龄)
TreeSet<Student> treeSet = new TreeSet<>();   // 默认使用自然排序
treeSet.add(new Student("张三", 20, 85));
treeSet.add(new Student("李四", 22, 90));
treeSet.add(new Student("王五", 20, 78));     // 年龄 20 与"张三"相同 → 触发 name 二次比较
treeSet.add(new Student("赵六", 21, 88));

for (Student s : treeSet) {
    System.out.println(s);
}

关键理解

  1. 构造方法传 Comparator = 覆盖默认排序:new TreeSet<>(comparator) 用的是比较器排序;new TreeSet<>() 用的是自然排序。
  2. Comparator 不改动实体类:排序规则写在集合这边,适合「类已定稿、临时按别的规则排序」的场景。
  3. 匿名内部类是 Comparator 最常用的实现方式(后续 Collections.sort 也用它)。

4. 返回值规则与 compareTo/equals 一致性的官方推荐

两种方式共用的返回值规则

返回值 含义 处理结果
负数 当前存入的元素比较小 存左边
0 当前存入的元素和已有元素「重复」 不存(TreeSet 去重 / TreeMap 键去重)
正数 当前存入的元素比较大 存右边

为什么官方强烈推荐「compareTo == 0 时 equals 也返回 true」?

因为这样能更好地存入 TreeMap / TreeSet 集合:

  • 如果我们设置的是根据年龄排序(或其他某个属性),那么当把这些对象存入 TreeMap/TreeSet 时,会因为某个属性相同导致对象存入不进集合。
  • 官方建议:如果 x1.compareTo(x2) == 0 或 compare(x1, x2) == 0,那么建议 x1.equals(x2) 返回 true,并且 hashCode 一致。

这样排序判定、相等判定、哈希判定三者保持一致,集合的「去重」行为才符合预期。


5. 20260731 课后作业解析:Map 集合基本操作与遍历

作业要求回顾

创建 HashMap<String, Integer> 保存学号-成绩,练习 put/size/get/containsKey/remove 等基本操作;分别用 keySet + get 和 entrySet 两种方式遍历;再创建 TreeMap 观察按键自动排序的效果。

代码解析

// 来源:homework0731/p2_map/MapDemo.java
package homework0731.p2_map;

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

public class MapDemo {
    public static void main(String[] args) {
        // 1. 创建 HashMap,添加 4 组成绩
        HashMap<String, Integer> hashMap = new HashMap<>();
        hashMap.put("x001", 90);
        hashMap.put("x002", 85);
        hashMap.put("x003", 92);
        hashMap.put("x004", 78);

        // 2. 输出集合大小
        int size = hashMap.size();               // 学生人数:4

        // 3. 查询 / 判断键是否存在
        // get("x002") → 85
        // containsKey("x001") → true,containsKey("x999") → false

        // 4. 修改:put("x004", 88)(键已存在 → 覆盖旧值)

        // 5. 删除:remove("x001")

        // 6. 遍历方式二:entrySet() + getKey()/getValue()
        Set<Map.Entry<String, Integer>> entries = hashMap.entrySet();
        for (Map.Entry<String, Integer> entry : entries) {
            System.out.println(entry.getKey() + "--->" + entry.getValue());
        }

        // 7. TreeMap:放入同样的 4 组数据,按键(学号)自动排序
        TreeMap<String, Integer> treeMap = new TreeMap<>();
        treeMap.put("x001", 90);
        treeMap.put("x002", 85);
        treeMap.put("x003", 92);
        treeMap.put("x004", 78);

        Set<String> set = treeMap.keySet();
        for (String key : set) {
            Integer value = treeMap.get(key);
            System.out.println(key + "--->" + value);
        }
    }
}

知识点总结

知识点 说明
put(key, value) 添加键值对;键已存在时覆盖旧值(即修改)
size() / get(key) 集合大小 / 根据键取值
containsKey(key) 判断键是否存在
remove(key) 根据键删除键值对
keySet() 遍历 先取所有键(Set<K>),再 get(key) 反查值
entrySet() 遍历 一次性拿到键值对 Entry,getKey()/getValue() 直接取值,效率更高
TreeMap 按键自动排序(学号 x001→x004 按字典序输出),与 HashMap 无序形成对比

6. 20260731 课后作业解析:异常处理综合(自定义异常)

作业要求回顾

循环输入 5 个成绩(0~100),用 try-catch 处理异常:输入非整数 → InputMismatchException;成绩不在 0~100 → 抛出自定义 InvalidScoreException;最后用迭代器遍历输出成绩。

自定义异常类

// 来源:homework0731/p4_exception/InvalidScoreException.java
package homework0731.p4_exception;

// 自定义运行时异常:继承 RuntimeException,构造方法传入错误信息
public class InvalidScoreException extends RuntimeException {
    public InvalidScoreException(String message) {
        super(message);
    }
}

主程序解析

// 来源:homework0731/p4_exception/ScoreStatisticsDemo.java
package homework0731.p4_exception;

import java.util.ArrayList;
import java.util.InputMismatchException;
import java.util.Iterator;
import java.util.Scanner;

public class ScoreStatisticsDemo {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        ArrayList<Integer> scores = new ArrayList<>();

        // 循环输入 5 个合法成绩;i 只有输入成功才自增(保证最终拿到 5 个合法成绩)
        for (int i = 0; i < 5; ) {
            try {
                int s = sc.nextInt();
                if (s < 0 || s > 100) {
                    throw new InvalidScoreException("成绩必须在0~100之间");  // 主动抛出自定义异常
                }
                scores.add(s);
                i++;
            } catch (InputMismatchException e) {   // 输入的不是整数
                System.out.println("请输入整数");
                sc.nextLine();                      // 清空缓冲区,避免死循环
            } catch (InvalidScoreException e) {     // 成绩越界
                System.out.println("成绩不合法:成绩必须在0~100之间");
            }
        }

        // 使用迭代器遍历输出所有成绩
        Iterator<Integer> iterator = scores.iterator();
        while (iterator.hasNext()) {
            Integer i = iterator.next();
            System.out.print(i + ", ");
        }
    }
}

知识点总结

知识点 说明
自定义异常 继承 RuntimeException(运行时异常)即可,通过构造方法传入 message
throw 主动抛出 throw new InvalidScoreException("...") 在业务逻辑中主动制造异常
多 catch 块 一个 try 可配多个 catch,分别捕获 InputMismatchException 和自定义异常
sc.nextLine() 清空缓冲 捕获 InputMismatchException 后必须清空输入缓冲,否则会陷入死循环
循环内 i++ 放在 try 内 只有输入合法才自增计数,保证最终得到 5 个合法成绩(程序更鲁棒)

7. 20260731 课后作业解析:匿名内部类 Comparator 排序

作业要求回顾

创建 ArrayList<Student>,用匿名内部类实现 Comparator<Student> 分别按年龄升序、成绩降序排序(Collections.sort);再创建 TreeSet<Student> 传入按姓名排序的匿名 Comparator,观察自动排序。

代码解析

// 来源:homework0731/p5_sort/SortDemo.java
package homework0731.p5_sort;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class SortDemo {
    public static void main(String[] args) {
        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));

        // 1. 匿名内部类实现 Comparator,按年龄升序排序
        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);
        }

        // 2. 匿名内部类 Comparator,按成绩(double)降序排序
        //    注意:double 不能直接相减强转,用 Double.compare 最安全
        Collections.sort(list, new Comparator<Student>() {
            @Override
            public int compare(Student s1, Student s2) {
                return Double.compare(s2.getScore(), s1.getScore());   // 成绩降序
            }
        });

        // 3. 创建 TreeSet<Student>,传入按姓名排序的匿名 Comparator,迭代器遍历观察自动排序
        // TreeSet<Student> treeSet = new TreeSet<>(comparator);  // 添加学生后自动按姓名排序
    }
}
// 来源:homework0731/p5_sort/Student.java(普通实体类,未实现 Comparable)
package homework0731.p5_sort;

public class 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;
    }

    // getter / setter ...(略)
    // toString() ...(略)
}

知识点总结

知识点 说明
Collections.sort(list, comparator) 对 List 集合用比较器排序(原地排序,改变原集合)
匿名内部类 Comparator 临时指定排序规则,无需修改 Student 类(本例 Student 未实现 Comparable)
Double.compare(a, b) double 类型比较的正确姿势:浮点数相减精度丢失,且无法强转 int;用 Double.compare 最安全
Comparator 降序技巧 交换 compare 中两个参数的位置即可反向:compare(s2, s1) = 降序
TreeSet + Comparator 无参构造用自然排序;传入 Comparator 的构造用比较器排序,存入即自动排序

8. 随堂练习要点

练习一:自然排序(Comparable)与比较器排序(Comparator)对比

核心步骤:

  1. Student 类实现 Comparable<Student>,compareTo 按年龄升序(年龄相同再按姓名)
  2. new TreeSet<>() 存入 4 名学生 → 观察自然排序结果
  3. new TreeSet<>(new Comparator<Student>() {...}) 按成绩升序 → 观察比较器排序结果
  4. 对比两种排序的输出顺序

思考:

  1. 为什么年龄相同的两名学生会「丢失」一个?—— compareTo 返回 0 视为重复,不存入;所以排序依据最好唯一,或叠加二次比较(姓名)
  2. 什么时候用 Comparable,什么时候用 Comparator?—— 类设计期确定默认规则用 Comparable;类已定稿需临时换规则用 Comparator

练习二:Map 集合基本操作与遍历(作业一)

核心步骤:

  1. HashMap 的 put/size/get/containsKey/remove 五类操作
  2. keySet() + get() 遍历;entrySet() + getKey()/getValue() 遍历
  3. TreeMap 用同样数据验证按键自动排序

思考:

  1. put 键已存在时是「添加」还是「修改」?—— 覆盖旧值,等价于修改
  2. keySet 遍历和 entrySet 遍历哪个效率高?—— entrySet 一次拿到键和值;keySet 还要 get(key) 反查一次

练习三:异常处理综合(作业二)

核心步骤:

  1. 自定义 InvalidScoreException extends RuntimeException
  2. 输入循环内 try-catch 多 catch 块(InputMismatchException / InvalidScoreException)
  3. 迭代器遍历输出成绩

思考:

  1. 捕获 InputMismatchException 后为什么要 sc.nextLine()?—— 清空输入缓冲区,否则错误输入会反复触发异常形成死循环
  2. 自定义异常继承 RuntimeException 和 Exception 有何区别?—— RuntimeException 是运行时异常,方法内抛出可不用显式声明 throws,编译不强制处理

练习四:匿名内部类 Comparator 排序(作业三)

核心步骤:

  1. Collections.sort(list, new Comparator<Student>(){...}) 按年龄升序
  2. 按成绩降序:Double.compare(s2.getScore(), s1.getScore())
  3. TreeSet<Student> 传入按姓名排序的匿名 Comparator,迭代器遍历

思考:

  1. double 成绩比较为什么不用 (int)(s1.getScore()-s2.getScore())?—— double 相减可能有精度误差,强转 int 也不安全;Double.compare 是标准做法
  2. 降序为什么交换参数位置即可?—— compare 返回值符号取反,排序方向随之反转

9. 拓展阅读

官方文档

推荐阅读

  • 《Java 核心技术 卷 I》第 9 章 集合 —— 排序与比较器
  • 《Effective Java》第 3 版 第 14 条:考虑实现 Comparable 接口
  • 《Java 编程思想》第 17 章 容器深入研究 —— 哈希码与排序

相关知识点

前置知识 当前知识 后续知识
匿名内部类(0727) Comparator 匿名内部类实现 Lambda 表达式(函数式接口)
集合体系与泛型(0729) TreeSet / TreeMap 有序集合 红黑树底层(0731 已讲)
HashMap 基本操作(0731) Map 综合练习(keySet/entrySet 遍历) Map 与 Set 的互转、分组统计
异常处理(0728) 自定义异常 + 多 catch 块 异常链、try-with-resources