# 20260803 课堂笔记 — 比较器排序(Comparable 自然排序 / Comparator 比较器排序)+ 集合工具类 Collections + 可变参数 Varargs + Lambda 表达式 + 20260731 课后作业解析 - **日期**:2026-08-03 - **项目**:`c260803` - **包路径**:`course` / `exericse` / `homework0731` - **作者**:WanJL --- ## 目录 1. [比较器概述:TreeSet / TreeMap 与两种排序方式](#1-比较器概述treeset--treemap-与两种排序方式) 2. [自然排序 Comparable(实体类内建排序规则)](#2-自然排序-comparable实体类内建排序规则) 3. [比较器排序 Comparator(临时自定义排序)](#3-比较器排序-comparator临时自定义排序) 4. [返回值规则与 compareTo/equals 一致性的官方推荐](#4-返回值规则与-comparetoequals-一致性的官方推荐) 5. [集合工具类 Collections](#5-集合工具类-collections) 6. [可变参数 Varargs(参数个数可变)](#6-可变参数-varargs参数个数可变) 7. [Lambda 表达式(函数式接口)](#7-lambda-表达式函数式接口) 8. [20260731 课后作业解析:Map 集合基本操作与遍历](#8-20260731-课后作业解析map-集合基本操作与遍历) 9. [20260731 课后作业解析:异常处理综合(自定义异常)](#9-20260731-课后作业解析异常处理综合自定义异常) 10. [20260731 课后作业解析:匿名内部类 Comparator 排序](#10-20260731-课后作业解析匿名内部类-comparator-排序) 11. [随堂练习要点](#11-随堂练习要点) 12. [拓展阅读](#12-拓展阅读) --- ## 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` 进行临时自定义排序。 ### 一个完整实体类的结构回顾 ```java // 来源: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)` 方法,根据**返回值**决定排序规则。这是类的**默认排序方式**。 ```java // 来源:course/Student.java package course; 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; } // 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` 并重写 `compare(Student o1, Student o2)`。 ```java // 来源:course/Demo01.java(部分) package course; import java.util.Comparator; import java.util.TreeSet; public class Demo01 { public static void main(String[] args) { // 按照自定义的要求临时进行排序:先按成绩升序,成绩相同再按姓名排序 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); } } } ``` ```java // 来源:course/Demo01.java(部分) // 不传 Comparator → 使用 Student 类自带的自然排序(Comparable:按年龄) TreeSet 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. 集合工具类 Collections ### 概念 `java.util.Collections` 是**操作集合的工具类**,专门用来对 `List` / `Set` / `Map` 等集合进行排序、查找、填充、转换等操作。 - 工具类里的方法**全部是静态方法**,**不需要创建对象**,直接通过类名调用 - 类比记忆:`Arrays` 是**数组**的工具类,`Collections` 就是**集合**的工具类 - 今天重点:`Collections.sort` 是**比较器排序**在 List 上的应用——传入 `Comparator` 匿名内部类即可对 List 自定义排序(与 TreeSet 构造传 Comparator 思路一致) ### 快速创建集合:List.of() ```java // 来源:course/Demo02.java(部分) // List.of() 是 JDK9+ 提供的快速创建【不可变集合】的方法 // 再用 new ArrayList<>(...) 包装成可变的 ArrayList List list = new ArrayList<>(List.of("C", "B", "A", "F", "G")); ``` ### 代码示例(Collections 常用方法) ```java // 来源:course/Demo02.java package course; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class Demo02 { public static void main(String[] args) { List list = new ArrayList<>(List.of("C", "B", "A", "F", "G")); // 1. 排序:自然顺序升序——要求每个元素都必须实现 Comparable 接口,重写 compareTo() Collections.sort(list); // 2. 排序:自定义方式排序——Comparator 匿名内部类,重写 compare() Collections.sort(list, new Comparator() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); // 3. 逆序:把集合元素顺序反转 Collections.reverse(list); // 4. 查找:二分查找指定元素,存在返回索引,不存在返回 -1 int i = Collections.binarySearch(list, "C"); // 5. 查找最大 / 最小元素 String max = Collections.max(list); String min = Collections.min(list); // 6. 把当前集合转化为不可变集合(不能 add / remove) List unmodifiableList = Collections.unmodifiableList(list); // 7. 随机打乱集合元素顺序 Collections.shuffle(list); // 8. 使用某个对象全部填充元素 Collections.fill(list, "X"); // 9. 交换两个位置的元素 Collections.swap(list, 0, 1); // 10. 旋转元素:整体向后移动 distance 个位置 Collections.rotate(list, 2); // 11. 替换指定元素:把旧值全部换成新值 Collections.replaceAll(list, "A", "Z"); } } ``` ### Collections 常用方法速查 | 方法 | 作用 | 说明 | |------|------|------| | `sort(List)` | 自然升序排序 | 元素**必须实现 Comparable**,否则抛 ClassCastException | | `sort(List, Comparator)` | 自定义排序 | 与 TreeSet(Comparator) 思路一致,按 compare 返回值排 | | `reverse(List)` | 逆序 | 把集合元素顺序整体反转 | | `binarySearch(List, key)` | 二分查找 | 返回元素索引,不存在返回 -1;**查找前必须先排序** | | `max(List)` / `min(List)` | 最大 / 最小元素 | 元素须可比较(实现 Comparable) | | `unmodifiableList(List)` | 转不可变集合 | 返回的集合不能增删改,否则抛 UnsupportedOperationException | | `shuffle(List)` | 随机打乱 | 类似洗牌 | | `fill(List, obj)` | 全部填充 | 用指定对象替换集合中所有元素 | | `swap(List, i, j)` | 交换元素 | 交换两个索引位置的元素 | | `rotate(List, distance)` | 旋转元素 | 元素整体向后移动 distance 个位置 | | `replaceAll(List, oldVal, newVal)` | 替换元素 | 把所有 oldVal 替换为 newVal | ### 关键理解 1. **Collections 与比较器的关系**:`Collections.sort(list, comparator)` 把**比较器排序**从「集合存入时」扩展到了「已有 List 排序」——二者共用同一套返回值规则(负数存左 / 0 相等 / 正数存右)。 2. **`sort(List)` 的前提**:自然排序要求元素实现 `Comparable`,否则运行时报 `ClassCastException`——与 TreeSet/TreeMap 存入对象的要求**完全一致**。 3. **`binarySearch` 使用前提**:二分查找基于有序数组,所以**查找前必须先 sort**,否则结果不可靠;找不到返回 -1。 4. **`List.of()` 的陷阱**:返回的是**不可变集合**,直接 `add`/`remove` 会抛 `UnsupportedOperationException`;Demo02 用 `new ArrayList<>(List.of(...))` 包装后才可变。 --- ## 6. 可变参数 Varargs(参数个数可变) ### 概念 **可变参数**也叫**参数个数可变**,作为方法的**形参**出现——也就是说**方法形参的个数是可以改变的**。 先回顾两个基础概念: - **形参**(形式参数):在**方法声明**的时候定义的参数,如 `public void method(int 形参名)` - **实参**(实际参数):在**调用方法**的时候传入的实际参数值,如 `setName("张三")`,其中的 `"张三"` 就是实际参数 **为什么要用可变参数?** 之前计算"多个整数相加",有多少个参数就要写多少个**方法重载**(`add(int a, int b)` / `add(int a, int b, int c)` / `add(int a, int b, int c, int d)`...)。这样**方便用户使用,但不利于我们自己定义**——写多少个重载都覆盖不完所有情况。有了可变参数,一个方法就能接收任意个数参数。 **格式**: ```java 访问权限修饰符 返回值类型 方法名(形参类型... 参数名){ // 方法体 } ``` ### 代码示例 ```java // 来源:course/Demo03.java package course; public class Demo03 { // 传统写法:几个参数就要重载几个方法 public int add(int a, int b) { return a + b; } /** * 可变参数:可以实现多个整数累加和 * @param a 就是一个可变参数(实际上等价于 int[] a) */ public int add(int... a) { // 本质上等价于 add(int[] a) int sum = 0; for (int n : a) { sum += n; } return sum; } public static void main(String[] args) { // 直接传入任意多个整数 int sum = new Demo03().add(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15); // 上面这一行,和下面这两行效果一样——可变参数本质就是数组 int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; int sum1 = new Demo03().add(arr); System.out.println(sum); } } ``` ### 注意事项 | 注意点 | 说明 | |--------|------| | **可变参数本质就是一个数组** | `int... a` 与 `int[] a` 等价,方法内用 foreach 遍历即可 | | **可变参数必须放在参数列表最后** | 一个方法有多个参数时,可变参数不能写在固定参数前面 | | **只能有一个可变参数** | 一个方法最多只能有一个可变参数 | | 调用方式灵活 | 既可以传 0 个、1 个、多个参数,也可以直接传一个数组 | ### 关键理解 1. **可变参数 = 数组的语法糖**:`int... a` 在方法内部就是一个 `int[] a`,所以遍历方式和数组完全一致。 2. **与重载的取舍**:参数个数不固定时用可变参数更简洁;参数个数固定、类型不同时用重载更清晰。 3. **与其他参数共存**:如果方法既有固定参数又有可变参数,可变参数必须写在最后,如 `public void method(String name, int... scores)`。 --- ## 7. Lambda 表达式(函数式接口) ### 概念 **Lambda 表达式**是 JDK 8 引入的、**简化匿名内部类**的一种写法,让代码更简洁、自由度更高。 **标准格式**: ```java (形式参数) -> {代码块} ``` - **形式参数**:多个参数用逗号分隔;没有参数就留空(如 `()`) - **`->`**:由英文减号 `-` 和大于号 `>` 组成,固定写法,代表**指向动作** - **{代码块}**:具体要做的事情,就是以前写在**方法体**中的内容 **三要素**:形式参数、箭头、代码块。 **为什么要有 Lambda 表达式?** Java 常被诟病"规范太多、太过冗长"。随着 JavaScript / Python / Go 等更灵活的语言流行,Java 为了迎合新的市场需求也开始求变,引入了 Lambda 表达式、函数式接口、Stream 流等更简练的写法。 ### 函数式接口(Functional Interface) **只有一个抽象方法**的接口叫**函数式接口**。Lambda 表达式本质是**另一种形式的匿名内部类**,但**只能用于简化函数式接口**——因为接口里只有一个需要重写的抽象方法,Lambda 的 `()` 才不会有歧义。 ```java // 来源:course/Swimming.java // Swimming 接口只有一个抽象方法 swim() → 函数式接口 public interface Swimming { void swim(); } ``` ### 代码示例:三种实现方式的对比 ```java // 来源:course/SwimmingTest.java package course; public class SwimmingTest { // 接收 Swimming 接口对象并调用其 swim() 方法 public static void getSwimming(Swimming s) { s.swim(); } public static void main(String[] args) { // 实现 Swimming 接口并重写 swim() 方法,以前有两种方式: // 1、建立实现类,创建实现类对象(此处略) // 2、创建 Swimming 的匿名内部类,并重写方法 getSwimming(new Swimming() { @Override public void swim() { System.out.println("自由泳.....自由....免费"); } }); // 3、使用 Lambda 表达式 —— 其实就是另一种形式的匿名内部类 getSwimming(() -> { System.out.println("蛙游.....呱....呱"); }); // 匿名内部类:用变量接收 Swimming swimming = new Swimming() { @Override public void swim() { System.out.println("自由泳.....自由....免费"); } }; swimming.swim(); // Lambda 表达式:用变量接收 Swimming sw = () -> { System.out.println("蛙游.....呱....呱"); }; sw.swim(); } } ``` ### 函数式编程思想 在数学中,函数就是"有输入量、输出量的一套计算方案",即**"拿数据做操作"**: - **面向对象思想**强调"必须通过对象的形式来做事情" - **函数式编程思想**则尽量忽略面向对象的复杂写法,强调"**做什么**"而不是"**以什么形式去做**" **Lambda 表达式就是体现函数式编程思想的一种实现方式。** ### JDK 内置函数式接口(java.util.function 包) 函数式接口:**接口中只包含一个抽象方法**的接口就是函数式接口。JDK 内置了大量函数式接口,基本都存在于 `java.util.function` 包中: | 接口 | 抽象方法形态 | 说明 | |------|--------------|------| | `Supplier` | 无参有返回值 | 返回类型 T | | `Consumer` | 有参无返回值 | 参数类型 T | | `Function` | 有参有返回值 | 参数类型 T,返回类型 R | | `Predicate` | 有参有返回值 | 参数类型 T,返回值 boolean(断言) | | `UnaryOperator` | 有参有返回值 | 接收 T,返回 T(一元运算) | | `BinaryOperator` | 有参有返回值 | 接收 T, T,返回 T(二元运算) | ### Lambda 省略模式 Demo04 中总结了三条**省略规则**: | 省略规则 | 说明 | |----------|------| | **① 参数类型可以省略** | 但不能"多个参数只省略其中一个"——要么全写,要么全省 | | **② 参数有且只有一个,小括号可以省略** | `(s) -> {...}` 可省略为 `s -> {...}` | | **③ 代码块的语句只有一条,可以省略大括号、分号以及 return 关键字** | `(a, b) -> { return a + b; }` 可省略为 `(a, b) -> a + b` | #### 省略模式示例一:有参有返回值接口 Addable ```java // 来源:course/Addable.java + AddableDemo.java // 有参有返回值的函数式接口 public interface Addable { int add(int x, int y); } public class AddableDemo { public static void useAdd(Addable a) { int sum = a.add(10, 20); System.out.println(sum); } public static void main(String[] args) { // 完整写法: // useAdd((a, b) -> { // return a + b; // }); // 省略写法:代码块只有一条语句 → 省略大括号、分号、return 关键字 useAdd((a, b) -> a + b); // 两个参数 → 小括号不能省略 } } ``` #### 省略模式示例二:有参无返回值接口 Flyable ```java // 来源:course/Flyable.java + FlyableDemo.java // 有参无返回值的函数式接口 public interface Flyable { void fly(String s); } public class FlyableDemo { public static void useFlyable(Flyable f) { f.fly("今天天气不错...是风和日丽的..."); } public static void main(String[] args) { // 参数有且只有一个 → 小括号可以省略:s -> {...} useFlyable(s -> { System.out.println(s); System.out.println("长翅膀....飞..."); }); } } ``` ### Lambda 使用前提 使用 Lambda 表达式有两个**必须满足**的前提: 1. **必须要有接口**——Lambda 的本质是简化接口的匿名实现 2. **接口中有且只有一个抽象方法**——即必须是函数式接口 ### Lambda 与匿名内部类的区别 | 对比维度 | 匿名内部类 | Lambda 表达式 | |----------|-----------|---------------| | **需要的类型** | 可以是**接口、抽象类、普通具体类** | **只能是接口** | | **使用限制** | 接口有多个抽象方法也能用 | **必须只有一个抽象方法**(函数式接口) | | **实现原理** | 编译后产生一个**独立的 `.class` 字节码文件** | 编译后**不产生单独的 `.class` 文件**,字节码在运行的时候**动态生成** | ### 关键理解 1. **Lambda = 匿名内部类的简化形式**:因为 `Swimming` 接口里**只有一个**需要重写的抽象方法,所以 `()` 能指向的就只有这一个抽象方法,`->` 指向的就是重写抽象方法的代码块内容。 2. **使用前提**:Lambda 只能简化**函数式接口**(只有一个抽象方法),多抽象方法接口用 Lambda 会编译报错。 3. **三要素对应**:`(形式参数)` → 重写方法的参数列表;`->` → 指向动作;`{代码块}` → 重写方法的方法体。 4. **省略模式记忆口诀**:类型可全省、单参可省括号、单语句可省大括号/分号/return。 5. **与匿名内部类的联系**:本节第 3 节 Comparator 正是用匿名内部类实现的,后续可以用 Lambda 进一步简化——如 `Collections.sort(list, (o1, o2) -> o1.getAge() - o2.getAge())`。 --- ## 8. 20260731 课后作业解析:Map 集合基本操作与遍历 ### 作业要求回顾 > 创建 `HashMap` 保存学号-成绩,练习 `put/size/get/containsKey/remove` 等基本操作;分别用 `keySet + get` 和 `entrySet` 两种方式遍历;再创建 `TreeMap` 观察按键自动排序的效果。 ### 代码解析 ```java // 来源: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 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> entries = hashMap.entrySet(); for (Map.Entry entry : entries) { System.out.println(entry.getKey() + "--->" + entry.getValue()); } // 7. TreeMap:放入同样的 4 组数据,按键(学号)自动排序 TreeMap treeMap = new TreeMap<>(); treeMap.put("x001", 90); treeMap.put("x002", 85); treeMap.put("x003", 92); treeMap.put("x004", 78); Set 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`),再 `get(key)` 反查值 | | **entrySet() 遍历** | 一次性拿到键值对 Entry,`getKey()/getValue()` 直接取值,效率更高 | | **TreeMap** | **按键自动排序**(学号 x001→x004 按字典序输出),与 HashMap 无序形成对比 | --- ## 9. 20260731 课后作业解析:异常处理综合(自定义异常) ### 作业要求回顾 > 循环输入 5 个成绩(0~100),用 try-catch 处理异常:输入非整数 → `InputMismatchException`;成绩不在 0~100 → 抛出自定义 `InvalidScoreException`;最后用迭代器遍历输出成绩。 ### 自定义异常类 ```java // 来源:homework0731/p4_exception/InvalidScoreException.java package homework0731.p4_exception; // 自定义运行时异常:继承 RuntimeException,构造方法传入错误信息 public class InvalidScoreException extends RuntimeException { public InvalidScoreException(String message) { super(message); } } ``` ### 主程序解析 ```java // 来源: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 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 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 个合法成绩(程序更鲁棒) | --- ## 10. 20260731 课后作业解析:匿名内部类 Comparator 排序 ### 作业要求回顾 > 创建 `ArrayList`,用**匿名内部类**实现 `Comparator` 分别按年龄升序、成绩降序排序(`Collections.sort`);再创建 `TreeSet` 传入按姓名排序的匿名 Comparator,观察自动排序。 ### 代码解析 ```java // 来源: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 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() { @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() { @Override public int compare(Student s1, Student s2) { return Double.compare(s2.getScore(), s1.getScore()); // 成绩降序 } }); // 3. 创建 TreeSet,传入按姓名排序的匿名 Comparator,迭代器遍历观察自动排序 // TreeSet treeSet = new TreeSet<>(comparator); // 添加学生后自动按姓名排序 } } ``` ```java // 来源: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 的构造用比较器排序,存入即自动排序 | --- ## 11. 随堂练习要点 ### 练习一:自然排序(Comparable)与比较器排序(Comparator)对比 **核心步骤**: 1. `Student` 类实现 `Comparable`,`compareTo` 按年龄升序(年龄相同再按姓名) 2. `new TreeSet<>()` 存入 4 名学生 → 观察自然排序结果 3. `new TreeSet<>(new Comparator() {...})` 按成绩升序 → 观察比较器排序结果 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(){...})` 按年龄升序 2. 按成绩降序:`Double.compare(s2.getScore(), s1.getScore())` 3. `TreeSet` 传入按姓名排序的匿名 Comparator,迭代器遍历 **思考**: 1. double 成绩比较为什么不用 `(int)(s1.getScore()-s2.getScore())`?—— double 相减可能有精度误差,强转 int 也不安全;`Double.compare` 是标准做法 2. 降序为什么交换参数位置即可?—— compare 返回值符号取反,排序方向随之反转 ### 练习五:Collections 工具类常用方法 **核心步骤**: 1. 用 `new ArrayList<>(List.of("C","B","A","F","G"))` 快速创建可变 List 2. `Collections.sort(list)` 自然排序 → 观察字母升序 3. `Collections.sort(list, new Comparator(){...})` 自定义排序(如降序) 4. `Collections.reverse(list)` / `Collections.shuffle(list)` 观察顺序变化 5. `Collections.binarySearch(list, "C")`(先排序再查找)、`Collections.max/min(list)` 6. `Collections.unmodifiableList(list)` 转不可变集合后尝试 `add()`,观察异常 7. `Collections.fill(list, "X")` / `Collections.swap(list, 0, 1)` / `Collections.replaceAll(list, "A", "Z")` **思考**: 1. `Collections.sort(list)` 为什么要求元素实现 `Comparable`?不实现会怎样?—— 工具类内部要比较元素大小,不实现则运行时抛 `ClassCastException` 2. `binarySearch` 查找前为什么要先排序?—— 二分查找基于有序序列,未排序结果不可靠 3. `List.of()` 返回的集合为什么不能 `add/remove`?—— 返回的是不可变集合,直接修改抛 `UnsupportedOperationException`,需用 `new ArrayList<>(...)` 包装 ### 练习六:可变参数(Varargs)—— 任意个数整数求和 **核心步骤**: 1. 定义一个方法 `add(int... a)`,方法内用 foreach 累加求和 2. 分别调用:`add(1, 2)`、`add(1, 2, 3)`、`add(1, 2, 3, 4, 5)`,体会"一个方法接收任意个数参数" 3. 用数组调用:`int[] arr = {...}; add(arr)`,验证"可变参数本质就是数组" 4. 定义带固定参数的方法:`method(String name, int... scores)`,验证可变参数必须放在最后 **思考**: 1. `int... a` 和 `int[] a` 有什么关系?—— 本质相同,`int... a` 是 `int[] a` 的语法糖 2. 为什么有了可变参数还要方法重载?—— 参数**个数**不确定时用可变参数;参数**个数固定、类型不同**时重载更清晰 3. 如果方法同时有固定参数和可变参数,顺序有什么要求?—— 可变参数必须写在**参数列表最后**,否则编译报错 ### 练习七:Lambda 表达式(简化匿名内部类) **核心步骤**: 1. 定义一个**函数式接口** `Swimming`(只有一个抽象方法 `void swim()`) 2. 用**匿名内部类**实现并调用 `swim()` 3. 用 **Lambda 表达式**实现同样的功能:`() -> { System.out.println("..."); }` 4. 分别用「方法参数传递」和「变量接收」两种方式使用 Lambda 5. 对比匿名内部类与 Lambda 的代码量 **思考**: 1. Lambda 表达式的三要素是什么?—— 形式参数、箭头 `->`、代码块 2. 为什么 Lambda 只能用在**只有一个抽象方法**的接口上?—— 只有一个待重写方法,`()` 才能唯一确定指向它 3. 本节第 3 节的 Comparator 匿名内部类能否改写成 Lambda?—— 可以,如 `(o1, o2) -> o1.getAge() - o2.getAge()`,后续 Stream 也会大量使用 ### 练习八:Lambda 省略模式与 JDK 内置函数式接口 **核心步骤**: 1. 定义**有参有返回值**函数式接口 `Addable`(`int add(int x, int y)`),用 Lambda 实现 `useAdd((a, b) -> a + b)`——验证"单语句省略大括号/分号/return" 2. 定义**有参无返回值**函数式接口 `Flyable`(`void fly(String s)`),用 Lambda 实现 `s -> {...}`——验证"单参数省略小括号" 3. 改写 Comparator:`Collections.sort(list, (s1, s2) -> s1.getAge() - s2.getAge())`,观察省略后的极简写法 4. 查阅 `java.util.function` 包,说出 `Supplier / Consumer / Function / Predicate` 四个接口的形态 **思考**: 1. 三条省略规则分别是什么?—— 参数类型可全省(不能只省一个)、单参数可省小括号、单语句可省大括号/分号/return 2. 为什么 `(a, b) -> a + b` 能省略 return?—— 代码块只有一条语句且是表达式时,表达式结果自动作为返回值 3. Lambda 与匿名内部类的本质区别是什么?—— 匿名内部类编译产生独立 .class 文件;Lambda 编译不产生 .class,运行期动态生成字节码 ### 随堂练习参考解答(已提供代码) 对应今日课堂练习的参考解答已写入授课代码: | 练习 | 参考代码路径 | |------|-------------| | 进阶 2:员工信息管理系统(Employee 实现 Comparable + equals/hashCode 一致性 + 匿名内部类 Comparator 按姓名排序) | [`exericse/exericse02/Employee.java`](./../授课代码/c260803/src/exericse/exericse02/Employee.java) + [`EmployeeSystem.java`](./../授课代码/c260803/src/exericse/exericse02/EmployeeSystem.java) | **Employee 参考解答要点**: ```java // 来源:exericse/exericse02/Employee.java // 自然排序:按薪资降序,薪资相同再按工号 @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:基于全部属性(salary + id + name + dept) @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Employee employee = (Employee) o; return Double.compare(salary, employee.salary) == 0 && Objects.equals(id, employee.id) && Objects.equals(name, employee.name) && Objects.equals(dept, employee.dept); } @Override public int hashCode() { return Objects.hash(id, name, salary, dept); } ``` **EmployeeSystem 参考解答要点**: ```java // 来源:exericse/exericse02/EmployeeSystem.java // 1. 默认自然排序:薪资降序 → 薪资相同按工号;工号相同的自动去重 TreeSet treeSet = new TreeSet<>(); treeSet.add(new Employee("E001", "张三", 12000, "技术部")); // ... 共添加 5 名员工 // 迭代器遍历输出 Iterator iterator = treeSet.iterator(); while (iterator.hasNext()) { Employee employee = iterator.next(); System.out.println(employee.getId() + "-" + employee.getName() + "-" + employee.getSalary() + "-" + employee.getDept()); } // 2. 临时需求:通讯录按姓名排序 → 重新创建 TreeSet 传入按姓名排序的匿名 Comparator treeSet = new TreeSet<>(new Comparator() { @Override public int compare(Employee o1, Employee o2) { return o1.getName().compareTo(o2.getName()); } }); ``` **原理思考**: 1. `E001-张三(12000)` 和 `E005-刘七(12000)` 薪资相同却能都存入?—— compareTo 薪资相同后继续比较工号(E001 ≠ E005),返回值不为 0,不视为重复 2. 注意:参考解答的 `equals` 基于**全部属性**(薪资+工号+姓名+部门),而 `compareTo` 只按**薪资+工号**判定——若两名员工薪资工号相同但姓名不同,会出现 `compareTo == 0` 但 `equals == false`。这与官方推荐(compareTo == 0 时 equals 返回 true)略有出入,实际开发中应尽量让两者判定口径保持一致。 --- ## 12. 拓展阅读 ### 官方文档 - [Comparable 接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Comparable.html) - [Comparator 接口 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Comparator.html) - [TreeSet (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/TreeSet.html) - [TreeMap (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/TreeMap.html) - [Collections 工具类 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Collections.html) - [List.of() (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/List.html#of()) - [可变参数 Varargs(Oracle 教程)](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html#varargs) - [Lambda 表达式(Oracle 教程)](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html) - [函数式接口(Oracle 教程)](https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#functional) - [java.util.function 包 (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/function/package-summary.html) - [RuntimeException (JDK 17)](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/RuntimeException.html) ### 推荐阅读 - 《Java 核心技术 卷 I》第 9 章 集合 —— 排序与比较器 - 《Effective Java》第 3 版 第 14 条:考虑实现 Comparable 接口 - 《Java 编程思想》第 17 章 容器深入研究 —— 哈希码与排序 - 《Java 8 实战》第 3 章 Lambda 表达式 —— 行为参数化、函数式接口、方法引用 ### 相关知识点 | 前置知识 | 当前知识 | 后续知识 | |----------|----------|----------| | 匿名内部类(0727) | Lambda 表达式 / 函数式接口 | Stream 流、方法引用 | | 接口与多态(0723/0724) | JDK 内置函数式接口(java.util.function) | 方法引用、Stream 高阶函数 | | 集合体系与泛型(0729) | TreeSet / TreeMap 有序集合 | 红黑树底层(0731 已讲) | | HashMap 基本操作(0731) | Map 综合练习(keySet/entrySet 遍历) | Map 与 Set 的互转、分组统计 | | 异常处理(0728) | 自定义异常 + 多 catch 块 | 异常链、try-with-resources | | Arrays 工具类(0716) | Collections 集合工具类 | Stream API、Java 8 新特性 | | Comparator 比较器排序(本节 3) | Collections.sort(list, comparator) | 函数式接口 & Lambda 简化比较器 | | 数组与方法参数(0717) | 可变参数 Varargs | 不定长参数 & 命令行参数、Stream 收集 |