20260803-笔记.md 43 KB

20260803 课堂笔记 — 比较器排序(Comparable 自然排序 / Comparator 比较器排序)+ 集合工具类 Collections + 可变参数 Varargs + Lambda 表达式 + 20260731 课后作业解析

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

目录

  1. 比较器概述:TreeSet / TreeMap 与两种排序方式
  2. 自然排序 Comparable(实体类内建排序规则)
  3. 比较器排序 Comparator(临时自定义排序)
  4. 返回值规则与 compareTo/equals 一致性的官方推荐
  5. 集合工具类 Collections
  6. 可变参数 Varargs(参数个数可变)
  7. Lambda 表达式(函数式接口)
  8. 20260731 课后作业解析:Map 集合基本操作与遍历
  9. 20260731 课后作业解析:异常处理综合(自定义异常)
  10. 20260731 课后作业解析:匿名内部类 Comparator 排序
  11. 随堂练习要点
  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 进行临时自定义排序。

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

// 来源: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. 集合工具类 Collections

概念

java.util.Collections 是操作集合的工具类,专门用来对 List / Set / Map 等集合进行排序、查找、填充、转换等操作。

  • 工具类里的方法全部是静态方法,不需要创建对象,直接通过类名调用
  • 类比记忆:Arrays 是数组的工具类,Collections 就是集合的工具类
  • 今天重点:Collections.sort 是比较器排序在 List 上的应用——传入 Comparator 匿名内部类即可对 List 自定义排序(与 TreeSet 构造传 Comparator 思路一致)

快速创建集合:List.of()

// 来源:course/Demo02.java(部分)
// List.of() 是 JDK9+ 提供的快速创建【不可变集合】的方法
// 再用 new ArrayList<>(...) 包装成可变的 ArrayList
List<String> list = new ArrayList<>(List.of("C", "B", "A", "F", "G"));

代码示例(Collections 常用方法)

// 来源: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<String> list = new ArrayList<>(List.of("C", "B", "A", "F", "G"));

        // 1. 排序:自然顺序升序——要求每个元素都必须实现 Comparable 接口,重写 compareTo()
        Collections.sort(list);

        // 2. 排序:自定义方式排序——Comparator 匿名内部类,重写 compare()
        Collections.sort(list, new Comparator<String>() {
            @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<String> 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)...)。这样方便用户使用,但不利于我们自己定义——写多少个重载都覆盖不完所有情况。有了可变参数,一个方法就能接收任意个数参数。

格式:

访问权限修饰符 返回值类型 方法名(形参类型... 参数名){
    // 方法体
}

代码示例

// 来源: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 引入的、简化匿名内部类的一种写法,让代码更简洁、自由度更高。

标准格式:

(形式参数) -> {代码块}
  • 形式参数:多个参数用逗号分隔;没有参数就留空(如 ())
  • ->:由英文减号 - 和大于号 > 组成,固定写法,代表指向动作
  • {代码块}:具体要做的事情,就是以前写在方法体中的内容

三要素:形式参数、箭头、代码块。

为什么要有 Lambda 表达式? Java 常被诟病"规范太多、太过冗长"。随着 JavaScript / Python / Go 等更灵活的语言流行,Java 为了迎合新的市场需求也开始求变,引入了 Lambda 表达式、函数式接口、Stream 流等更简练的写法。

函数式接口(Functional Interface)

只有一个抽象方法的接口叫函数式接口。Lambda 表达式本质是另一种形式的匿名内部类,但只能用于简化函数式接口——因为接口里只有一个需要重写的抽象方法,Lambda 的 () 才不会有歧义。

// 来源:course/Swimming.java
// Swimming 接口只有一个抽象方法 swim() → 函数式接口
public interface Swimming {
    void swim();
}

代码示例:三种实现方式的对比

// 来源: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> 无参有返回值 返回类型 T
Consumer<T> 有参无返回值 参数类型 T
Function<T, R> 有参有返回值 参数类型 T,返回类型 R
Predicate<T> 有参有返回值 参数类型 T,返回值 boolean(断言)
UnaryOperator<T> 有参有返回值 接收 T,返回 T(一元运算)
BinaryOperator<T> 有参有返回值 接收 T, T,返回 T(二元运算)

Lambda 省略模式

Demo04 中总结了三条省略规则:

省略规则 说明
① 参数类型可以省略 但不能"多个参数只省略其中一个"——要么全写,要么全省
② 参数有且只有一个,小括号可以省略 (s) -> {...} 可省略为 s -> {...}
③ 代码块的语句只有一条,可以省略大括号、分号以及 return 关键字 (a, b) -> { return a + b; } 可省略为 (a, b) -> a + b

省略模式示例一:有参有返回值接口 Addable

// 来源: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

// 来源: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<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 无序形成对比

9. 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 个合法成绩(程序更鲁棒)

10. 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 的构造用比较器排序,存入即自动排序

11. 随堂练习要点

练习一:自然排序(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 返回值符号取反,排序方向随之反转

练习五:Collections 工具类常用方法

核心步骤:

  1. 用 new ArrayList<>(List.of("C","B","A","F","G")) 快速创建可变 List
  2. Collections.sort(list) 自然排序 → 观察字母升序
  3. Collections.sort(list, new Comparator<String>(){...}) 自定义排序(如降序)
  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 + EmployeeSystem.java

Employee 参考解答要点:

// 来源: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 参考解答要点:

// 来源:exericse/exericse02/EmployeeSystem.java
// 1. 默认自然排序:薪资降序 → 薪资相同按工号;工号相同的自动去重
TreeSet<Employee> treeSet = new TreeSet<>();
treeSet.add(new Employee("E001", "张三", 12000, "技术部"));
// ... 共添加 5 名员工

// 迭代器遍历输出
Iterator<Employee> 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<Employee>() {
    @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. 拓展阅读

官方文档

推荐阅读

  • 《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 收集