|
|
@@ -0,0 +1,1091 @@
|
|
|
+# 20260806 课堂笔记 — 文件操作类 File(java.io.File)+ IO 流入门(字节流 + 文件字节输出流 FileOutputStream)+ JavaSE 综合大作业(20260804 课后作业)解析:学生信息管理系统(Person 继承体系 + 接口 + 自定义异常 + 集合 + 比较器排序 + Lambda/Stream 选做挑战)
|
|
|
+
|
|
|
+- **日期**:2026-08-06
|
|
|
+- **项目**:`c260806`
|
|
|
+- **包路径**:`course` / `homework0804`(`entity` / `exception` / `manager` / `main` / `util` 分层)
|
|
|
+- **作者**:WanJL
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+1. [文件操作类 File:java.io.File 的构造 / 创建 / 删除 / 判断 / 获取](#1-文件操作类-filejavaiofile-的构造--创建--删除--判断--获取)
|
|
|
+2. [IO 流入门:字节流(FileOutputStream 输出 / FileInputStream 输入 / 文件复制)](#2-io-流入门字节流fileoutputstream-输出--fileinputstream-输入--文件复制)
|
|
|
+3. [JavaSE 综合大作业(20260804 课后作业)解析:学生信息管理系统](#3-javase-综合大作业20260804-课后作业解析学生信息管理系统)
|
|
|
+ - [3.1 作业目标与项目分层结构](#31-作业目标与项目分层结构)
|
|
|
+ - [3.2 实体层 entity:抽象类 Person 与继承体系](#32-实体层-entity抽象类-person-与继承体系)
|
|
|
+ - [3.3 实体层 entity:接口 Studyable / Teachable 与多接口实现](#33-实体层-entity接口-studyable--teachable-与多接口实现)
|
|
|
+ - [3.4 实体层 entity:对象关联(Teacher → Course)与 toString 重写](#34-实体层-entity对象关联teacher--course与-tostring-重写)
|
|
|
+ - [3.5 实体层 entity:Score 成绩类 —— setter 校验 + 自定义异常 + equals/hashCode](#35-实体层-entityscore-成绩类--setter-校验--自定义异常--equalshashcode)
|
|
|
+ - [3.6 异常层 exception:四个自定义异常类的统一写法](#36-异常层-exception四个自定义异常类的统一写法)
|
|
|
+ - [3.7 管理层 manager:StudentManager(ArrayList + 迭代器 + 增删改查)](#37-管理层-managerstudentmanagerarraylist--迭代器--增删改查)
|
|
|
+ - [3.8 管理层 manager:CourseManager(Stream + forEach + 方法引用)](#38-管理层-managercoursemanagerstream--foreach--方法引用)
|
|
|
+ - [3.9 管理层 manager:ScoreManager(HashMap 按学号分组 + 及格率统计)](#39-管理层-managerscoremanagerhashmap-按学号分组--及格率统计)
|
|
|
+ - [3.10 工具层 util:RankUtil —— Comparable / Comparator / Lambda 三种排序方式](#310-工具层-utilrankutil--comparable--comparator--lambda-三种排序方式)
|
|
|
+ - [3.11 入口 main:StudentSystemApp —— Scanner + while + switch + 标签 break 菜单系统](#311-入口-mainstudentsystemapp--scanner--while--switch--标签-break-菜单系统)
|
|
|
+4. [知识点全景总结](#4-知识点全景总结)
|
|
|
+5. [随堂练习要点](#5-随堂练习要点)
|
|
|
+6. [拓展阅读](#6-拓展阅读)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 文件操作类 File:java.io.File 的构造 / 创建 / 删除 / 判断 / 获取
|
|
|
+
|
|
|
+### 概念 —— 文件与目录的抽象表示
|
|
|
+
|
|
|
+`java.io.File` 是**文件和目录路径名的抽象表示**:文件(File)和目录(文件夹)都可以封装成 File 对象。注意:**File 封装的不一定是真实存在的文件,只是一个路径名**——它可以是存在的,也可以是不存在的,需要将来通过这个路径进行具体操作,把文件路径内容转换为真实存在的。
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
+/*
|
|
|
+ java.io.File
|
|
|
+ File类,是文件和目录路径名的抽象表示(类)
|
|
|
+ 文件和目录(文件夹)可以通过File封装成对象
|
|
|
+ 对于File来说,他的封装并不是一个真正存在的文件,只是一个路径名,他是可以存在,也可以不存在,
|
|
|
+ 需要将来通过这个路径进行具体的操作,把文件路径内容转换为真实存在的。
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:File 类位于 `java.io` 包(IO 入门第一课),它**只描述路径、不做真正的读写**;真正读写文件内容需要后续的 IO 流(InputStream/OutputStream/Reader/Writer)。
|
|
|
+
|
|
|
+### 三种构造方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
+public File(String pathname) // 通过给定的路径名字符串转换为抽象路径名
|
|
|
+public File(String parent, String child) // 从父路径字符串和子路径字符串创建
|
|
|
+public File(File parent, String child) // 从父抽象路径名(File对象)和子路径名字符串创建
|
|
|
+```
|
|
|
+
|
|
|
+### 路径分隔符 + 绝对路径 vs 相对路径
|
|
|
+
|
|
|
+| 概念 | 说明 |
|
|
|
+|------|------|
|
|
|
+| **路径分隔符** | Windows 是 `\`;Linux / UNIX / 安卓 / iOS 都是 `/` |
|
|
|
+| **绝对路径** | 完整路径,从盘符(C盘/D盘)或根目录 `/` 开始,如 `E:/abc/a.txt`(可理解为快递地址/经纬度) |
|
|
|
+| **相对路径** | 简化的路径,相对于某个位置(可理解为前后左右),如 `./` 表示当前项目的同一层级,`../` 表示当前项目的上一层级 |
|
|
|
+
|
|
|
+### 创建 / 删除 / 判断 / 获取 功能方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo01.java(注释部分)
|
|
|
+// 创建文件及目录
|
|
|
+public boolean createNewFile() // 该文件不存在时创建新文件,会抛异常 IOException
|
|
|
+public boolean mkdir() // 创建新文件夹(单级)
|
|
|
+public boolean mkdirs() // 创建新文件夹(多级),父目录不存在则一并创建
|
|
|
+// 删除文件或目录
|
|
|
+public boolean delete() // 删除文件或目录
|
|
|
+// 判断
|
|
|
+public boolean isDirectory() // 判断是否是文件夹
|
|
|
+public boolean isFile() // 判断是否是文件
|
|
|
+public boolean exists() // 判断该路径是否真实存在
|
|
|
+// 获取功能
|
|
|
+public String getAbsolutePath() // 获取绝对路径
|
|
|
+public String getPath() // 获取路径
|
|
|
+public String getName() // 获取文件名或目录名
|
|
|
+public File[] listFiles() // 返回该路径目录下所有文件和目录组成的数组
|
|
|
+```
|
|
|
+
|
|
|
+### main 演示 —— 相对路径操作 + 六大获取/判断方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo01.java
|
|
|
+public static void main(String[] args) throws IOException {
|
|
|
+ File file = new File("./"); // 相对路径:当前项目同层级
|
|
|
+ //file.createNewFile(); // 创建文件(会抛 IOException,已注释)
|
|
|
+ boolean b = file.mkdirs(); // 多级创建文件夹
|
|
|
+ //boolean d = file.delete(); // 删除(已注释)
|
|
|
+
|
|
|
+ System.out.println("是否创建成功:" + b);
|
|
|
+ System.out.println("绝对路径:" + file.getAbsolutePath());
|
|
|
+ System.out.println("相对路径:" + file.getPath());
|
|
|
+ System.out.println("是否是文件:" + file.isFile());
|
|
|
+ System.out.println("是否是文件夹:" + file.isDirectory());
|
|
|
+ System.out.println("是否存在:" + file.exists());
|
|
|
+ System.out.println("文件名:" + file.getName());
|
|
|
+ System.out.println("里面有哪些文件:" + Arrays.toString(file.listFiles()));
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:
|
|
|
+> - `createNewFile()` 声明抛 **`IOException`**,所以 main 方法用 `throws IOException` 声明(编译期异常)
|
|
|
+> - `mkdirs()` vs `mkdir()`:前者**多级递归创建**父目录,更常用
|
|
|
+> - `listFiles()` 返回 **`File[]` 数组**,用 `Arrays.toString()` 打印可看到目录下所有文件/子目录
|
|
|
+> - `./` 相对路径指向**当前项目目录**,是后续文件读写的起点概念
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. IO 流入门:字节流(FileOutputStream 输出 / FileInputStream 输入 / 文件复制)
|
|
|
+
|
|
|
+### 概念 —— 什么是 IO 流
|
|
|
+
|
|
|
+IO 是**输入输出**(Input/Output);流(Stream)是一种**抽象概念**,是对数据传输的总称——数据在设备之间的传输称为流,流的本质就是**数据传输**。IO 流用来处理设备之间的数据传输问题,常见应用场景:**文件复制、文件上传、文件下载**。
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
+/*
|
|
|
+ IO流
|
|
|
+ IO:输入输出(Input/Output)
|
|
|
+ 流:Stream是一种抽象概念,是对数据传输的总称,也就是说数据在设备直接的传输称为流,流的本质就是数据传输
|
|
|
+ IO流就是用来处理设备之间的数据传输问题的,常见应用场景:文件赋值、文件上传、文件下载。
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### IO 流的分类(两个维度)
|
|
|
+
|
|
|
+| 分类维度 | 类别 | 说明 |
|
|
|
+|----------|------|------|
|
|
|
+| **按数据流向**(相对于内存) | 输入流 | 向内存输入数据(读数据) |
|
|
|
+| | 输出流 | 从内存往外输出数据(写数据) |
|
|
|
+| **按数据类型** | 字节流 | 传输的每个数据都是 byte 类型(字节输入流 / 字节输出流) |
|
|
|
+| | 字符流 | 传输的每个数据都是 char 类型(字符输入流 / 字符输出流) |
|
|
|
+
|
|
|
+### IO 流的使用场景推荐
|
|
|
+
|
|
|
+- **纯文本文件(.txt)**:建议使用**字符流**,因为 1 个字符等于 2 个字节
|
|
|
+- **除纯文本之外的任何文件**(Word / Excel / PPT / md / png / mp3 / mp4 / rmvb 等):建议**字节流**
|
|
|
+- **不确定文件类型**:用**字节流**
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
+/*
|
|
|
+ 如果是纯文本文件(.txt),建议使用字符流,因为1个字符等于2个字节
|
|
|
+ 除了纯文本文件之外的任何文件包括文本、图片、视频、音频、等等任何文件,都建议是字节流。
|
|
|
+ 如果不确定文件类型,就是有字节流
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### InputStream 与 OutputStream —— 字节流两大抽象父类
|
|
|
+
|
|
|
+- `OutputStream`:抽象类,表示**所有字节输出流**类的父类
|
|
|
+- `InputStream`:抽象类,表示**所有字节输入流**类的父类
|
|
|
+- 子类命名特点:**子类的名称都以父类类名作为子类的后缀**(如文件字节输入流 `FileInputStream`、文件字节输出流 `FileOutputStream`)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
+/*
|
|
|
+ OutputStream:这是一个抽象类,表示所有字节输出流类的父类
|
|
|
+ InputStream:这是一个抽象类,表示所有字节输入流类的父类
|
|
|
+ 文件字节输入流:FileInputStream
|
|
|
+ 文件字节输出流:FileOutputStream
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### 使用字节输出流的三个步骤
|
|
|
+
|
|
|
+1. 创建字节输出流对象
|
|
|
+2. 调用字节输出流的写数据方法
|
|
|
+3. 释放资源,关闭流
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
+/*
|
|
|
+ 使用字节输出流的步骤:
|
|
|
+ 1、创建字节输出流对象
|
|
|
+ 2、调用字节输出流的写数据的方法
|
|
|
+ 3、释放资源,关闭流
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### FileOutputStream 的四种构造方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileOutputStreamTest.java(注释部分)
|
|
|
+FileOutputStream(File file) // 创建输出流,参数为 File 对象
|
|
|
+FileOutputStream(String name) // 创建输出流,参数为字符串路径
|
|
|
+FileOutputStream(File file, boolean append) // append 为 true 表示追加内容(否则覆盖)
|
|
|
+FileOutputStream(String name, boolean append) // 字符串路径 + 追加开关
|
|
|
+```
|
|
|
+
|
|
|
+### write() 三种写数据方法
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileOutputStreamTest.java(注释部分)
|
|
|
+write(int b) // 写一个字节
|
|
|
+write(byte[] b) // 写一个字节数组的内容
|
|
|
+write(byte[] b, int off, int len) // 写字节数组的内容,从 off 开始,长度为 len
|
|
|
+```
|
|
|
+
|
|
|
+### main 演示 —— 三步走写入文件
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileOutputStreamTest.java
|
|
|
+public static void main(String[] args) throws IOException {
|
|
|
+ // 使用字节输出流的步骤:
|
|
|
+ // 1、创建字节输出流对象(append=true 表示追加写入)
|
|
|
+ FileOutputStream fos = new FileOutputStream("E:/我的文件.txt", true);
|
|
|
+
|
|
|
+ // 2、调用字节输出流的写数据方法
|
|
|
+ fos.write(97); // 写一个字节(ASCII 97 = 'a')
|
|
|
+ fos.write(98); // 'b'
|
|
|
+ fos.write(99); // 'c'
|
|
|
+
|
|
|
+ byte[] arr = {65, 66, 67, 68, 69, 70, 71, 72}; // 对应 "ABCDEFGH"
|
|
|
+ fos.write(arr); // 写整个字节数组
|
|
|
+ fos.write(arr, 3, 3); // 从下标 3 开始写 3 个:'D' 'E' 'F'
|
|
|
+ fos.write("\r\n".getBytes()); // 写入换行(Windows 是 \r\n)
|
|
|
+
|
|
|
+ String s = """
|
|
|
+ 两只黄鹂鸣翠柳,
|
|
|
+ 一行白鹭上青天,
|
|
|
+ 窗含西岭千秋雪,
|
|
|
+ 门泊自挂东南枝。
|
|
|
+ """;
|
|
|
+ byte[] bytes = s.getBytes(); // 字符串 → 字节数组
|
|
|
+ fos.write(bytes);
|
|
|
+
|
|
|
+ // 3、释放资源,关闭流
|
|
|
+ fos.close();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 换行符对比
|
|
|
+
|
|
|
+| 系统 | 换行符 |
|
|
|
+|------|--------|
|
|
|
+| Linux | `\n` |
|
|
|
+| Windows | `\r\n` |
|
|
|
+| macOS | `\r` |
|
|
|
+
|
|
|
+> **要点**:
|
|
|
+> - `FileOutputStream` 是**字节输出流**,写入的是 byte 数据;字符串需先 `getBytes()` 转成字节数组
|
|
|
+> - `append=true` 是**追加写**(不会覆盖原内容);默认不带 append 参数是**覆盖写**
|
|
|
+> - 流用完之后**必须 `close()` 释放资源**,否则文件句柄不释放、数据可能没刷入磁盘
|
|
|
+> - `write(int b)` 传入的整数实际写的是低 8 位字节(ASCII 码),97→'a'、65→'A'
|
|
|
+> - main 方法声明 `throws IOException`,因为构造和 write 都可能抛编译期异常
|
|
|
+
|
|
|
+### 使用字节输入流的步骤(与输出流对称)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/Demo02.java(注释部分)
|
|
|
+/*
|
|
|
+ 使用字节输入流的步骤:
|
|
|
+ 1、创建字节输入流对象
|
|
|
+ 2、调用字节输入流的读数据的方法
|
|
|
+ 3、释放资源,关闭流
|
|
|
+*/
|
|
|
+```
|
|
|
+
|
|
|
+### FileInputStream —— 文件字节输入流
|
|
|
+
|
|
|
+**读取方法**(与 write 三种写法一一对应):
|
|
|
+- `read()`:读取一个字节
|
|
|
+- `read(byte[] b)`:一次读取数据存入到字节数组 b 中
|
|
|
+- `read(byte[] b, int off, int len)`:读入字节数组,从 off 开始,长度为 len
|
|
|
+
|
|
|
+> **关键返回值**:`read()` 返回 **-1 表示读到文件末尾(EOF)**;`read(byte[] b)` 返回**本次读取到的有效字节长度**。
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileInputStreamTest.java
|
|
|
+public static void main(String[] args) {
|
|
|
+ // 使用字节输入流的步骤:
|
|
|
+ FileInputStream fis = null;
|
|
|
+ try {
|
|
|
+ // 1、创建字节输入流对象
|
|
|
+ fis = new FileInputStream("E:/我的文件.txt");
|
|
|
+ // 2、调用字节输入流的读数据方法
|
|
|
+ byte[] bytes = new byte[1024]; // 1KB 缓冲区
|
|
|
+ System.out.println(fis.read()); // 读取一个字节并输出
|
|
|
+
|
|
|
+ int length; // 定义每一次读取的字节数量
|
|
|
+ // fis.read(bytes) 的返回值是本次读取到的有效字节长度;-1 表示读完
|
|
|
+ while ((length = fis.read(bytes)) != -1) {
|
|
|
+ System.out.println(new String(bytes, 0, length, Charset.forName("UTF-8")));
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ } finally { // finally 中写 close() 关闭流
|
|
|
+ try {
|
|
|
+ // 3、释放资源,关闭流
|
|
|
+ fis.close();
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:
|
|
|
+> - **循环读**是经典写法:`while ((length = fis.read(bytes)) != -1)` —— 边读边转,`-1` 是读取结束的哨兵值
|
|
|
+> - 字节数组转字符串时指定编码:`new String(bytes, 0, length, Charset.forName("UTF-8"))`(0 起、读 length 个有效字节,避免把缓冲区的脏数据也转进去)
|
|
|
+> - 用 **try-catch-finally 关闭资源**:`finally` 保证无论是否发生异常都会执行 `close()`
|
|
|
+> - `read(byte[])` 每次读一批(缓冲区思想),比逐字节 `read()` 效率高得多
|
|
|
+
|
|
|
+### 文件复制 FileCopy —— 输入流 + 输出流配合
|
|
|
+
|
|
|
+**文件复制 = 输入流读 + 输出流写**:用 `FileInputStream` 从源文件读到缓冲区,再用 `FileOutputStream` 把缓冲区写入目标文件,循环直到读完。
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileCopy.java
|
|
|
+public static void copy(String src, String desc) throws IOException {
|
|
|
+ // 1、创建输入流对象
|
|
|
+ FileInputStream fis = new FileInputStream(src);
|
|
|
+ // 2、创建输出流对象
|
|
|
+ FileOutputStream fos = new FileOutputStream(desc);
|
|
|
+
|
|
|
+ // 3、创建一个字节数组作为缓冲区(每次最多复制 4096 字节)
|
|
|
+ byte[] bytes = new byte[4096];
|
|
|
+ int length; // 每次读取并复制的字节数量
|
|
|
+ while ((length = fis.read(bytes)) != -1) { // 循环读取,-1 表示读完
|
|
|
+ fos.write(bytes, 0, length); // 循环写入本次有效字节
|
|
|
+ }
|
|
|
+
|
|
|
+ // 释放资源
|
|
|
+ fos.close();
|
|
|
+ fis.close();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:course/FileCopy.java(main 调用 + 耗时统计)
|
|
|
+public static void main(String[] args) throws IOException {
|
|
|
+ String src = "E:\\...\\14_git下载安装.mp4"; // 源文件(视频)
|
|
|
+ String desc = "D:\\abc.mp4"; // 目标文件
|
|
|
+ long startTime = System.currentTimeMillis(); // 复制前时刻(1970 至今的毫秒数)
|
|
|
+ copy(src, desc);
|
|
|
+ long endTime = System.currentTimeMillis(); // 复制后时刻
|
|
|
+ System.out.println(endTime - startTime); // 输出复制耗时(毫秒)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:
|
|
|
+> - **缓冲区思想**:`byte[]` 一次读/写一批,而不是一个字节一个字节操作,大幅提升效率(视频、图片等大文件复制必备)
|
|
|
+> - `read(bytes)` 返回有效字节数,`write(bytes, 0, length)` 只写入本次读到的有效部分,**防止把缓冲区残留数据也写进去**
|
|
|
+> - 关闭顺序:先关输出流(fos),再关输入流(fis)
|
|
|
+> - `System.currentTimeMillis()` 返回从 1970-01-01 起的毫秒数,两时刻相减即可统计复制耗时
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. JavaSE 综合大作业(20260804 课后作业)解析:学生信息管理系统
|
|
|
+
|
|
|
+### 3.1 作业目标与项目分层结构
|
|
|
+
|
|
|
+### 作业目标
|
|
|
+
|
|
|
+这是 **20260804 课后作业 —— JavaSE 综合大作业**,把整个 JavaSE 阶段学过的核心知识点**综合**到一个小型"学生信息管理系统"中,核心功能:
|
|
|
+
|
|
|
+- **学生管理**:添加 / 删除 / 查找 / 查看所有学生(学号唯一,重复抛异常)
|
|
|
+- **课程管理**:课程编号唯一(重复不允许添加)
|
|
|
+- **成绩管理**:录入成绩(0~100 校验)、按学号求总分 / 平均分、及格率统计
|
|
|
+- **成绩排名**:TreeSet 自然排序、Comparator 按姓名 / 按年龄(Lambda)/ 按平均分降序
|
|
|
+- **高级查询**:挑战 · 选做(Stream 流)
|
|
|
+
|
|
|
+### 分层结构
|
|
|
+
|
|
|
+```
|
|
|
+src/homework0804/
|
|
|
+├── entity/ 实体类:Person、Student、Teacher、Course、Score + 接口 Studyable/Teachable
|
|
|
+├── exception/ 自定义异常:Course/DuplicateStudent/ScoreOutOfBounds/StudentNotFound + Exception
|
|
|
+├── manager/ 管理类:StudentManager、CourseManager、ScoreManager(业务逻辑)
|
|
|
+├── main/ 入口类:StudentSystemApp(main 方法 + 菜单)
|
|
|
+└── util/ 工具类:RankUtil(排序)
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/main/StudentSystemApp.java(包导入)
|
|
|
+package homework0804.main;
|
|
|
+import homework0804.entity.*;
|
|
|
+import homework0804.exception.*;
|
|
|
+import homework0804.manager.*;
|
|
|
+import homework0804.util.*;
|
|
|
+```
|
|
|
+
|
|
|
+> **设计思想**:按职责把代码拆成 `entity`(数据)/ `exception`(错误)/ `manager`(业务)/ `util`(工具)/ `main`(入口)五个包,是后续分层架构(如三层架构 Controller/Service/DAO)的雏形。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.2 实体层 entity:抽象类 Person 与继承体系
|
|
|
+
|
|
|
+### 抽象类 Person —— 抽取共性
|
|
|
+
|
|
|
+把"学生 / 教师"的**公共属性(姓名、年龄)**抽到父类 `Person`,并声明**抽象方法 `getRole()`** 强制子类返回角色描述:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Person.java
|
|
|
+public abstract class Person {
|
|
|
+ private String name; // 姓名
|
|
|
+ private int age; // 年龄
|
|
|
+
|
|
|
+ public Person(String name, int age) {
|
|
|
+ this.name = name;
|
|
|
+ this.age = age;
|
|
|
+ }
|
|
|
+ // getter / setter 省略...
|
|
|
+ // 抽象方法 —— 返回角色描述("学生" / "教师")
|
|
|
+ public abstract String getRole();
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return "姓名:" + name + ",年龄:" + age;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Student —— 继承 + 接口多实现
|
|
|
+
|
|
|
+`Student` 继承 `Person`,同时实现 `Comparable<Student>` 和 `Studyable` 两个接口,**「继承 + 多接口」**是 Java 多继承能力的替代方案:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java
|
|
|
+public class Student extends Person implements Comparable<Student>, Studyable {
|
|
|
+ private String id; // 学号,如 S001
|
|
|
+ private String clazz; // 班级,如 "Java2301"
|
|
|
+ private List<Score> scores; // 该学生的成绩列表(对象关联)
|
|
|
+
|
|
|
+ public Student(String name, int age, String id, String clazz) {
|
|
|
+ super(name, age); // 调用父类构造方法(继承知识点)
|
|
|
+ this.id = id;
|
|
|
+ this.clazz = clazz;
|
|
|
+ this.scores = new ArrayList<>();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 子类必须实现抽象方法 getRole()
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java
|
|
|
+@Override
|
|
|
+public String getRole() {
|
|
|
+ return "学生";
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:抽象类不能被实例化,子类必须重写所有抽象方法(否则子类也必须是抽象类)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.3 实体层 entity:接口 Studyable / Teachable 与多接口实现
|
|
|
+
|
|
|
+### 接口定义 —— 规范"能做什么"
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Studyable.java
|
|
|
+public interface Studyable {
|
|
|
+ void study(); // 学习
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Teachable.java
|
|
|
+public interface Teachable {
|
|
|
+ void teach(); // 授课
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 接口实现
|
|
|
+
|
|
|
+`Student` 实现 `Studyable`,`Teacher` 实现 `Teachable`:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java
|
|
|
+@Override
|
|
|
+public void study() {
|
|
|
+ System.out.println(this.getName() + " 同学正在努力学习...");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Teacher.java
|
|
|
+@Override
|
|
|
+public void teach() {
|
|
|
+ System.out.println(this.getName() + "老师正在讲授《" + this.course.getCourseName() + "》...");
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:接口方法默认 `public abstract`,实现类必须 `public` 重写;接口是"契约",体现**面向接口编程**思想。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.4 实体层 entity:对象关联(Teacher → Course)与 toString 重写
|
|
|
+
|
|
|
+### Teacher 持有 Course —— 对象作为成员变量
|
|
|
+
|
|
|
+一个教师"教一门课",所以 `Teacher` 里直接持有一个 `Course` 对象,这就是**对象关联 / 对象组合**:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Teacher.java
|
|
|
+public class Teacher extends Person implements Teachable {
|
|
|
+ private String id; // 工号,如 T001
|
|
|
+ private Course course; // 教授的课程(对象关联)
|
|
|
+
|
|
|
+ public Teacher(String name, int age, String id, Course course) {
|
|
|
+ super(name, age);
|
|
|
+ this.id = id;
|
|
|
+ this.course = course;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Course —— 最普通的 Bean 类
|
|
|
+
|
|
|
+`Course` 只含属性 + 构造方法 + getter/setter + toString,是标准的 **JavaBean**:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Course.java
|
|
|
+public class Course {
|
|
|
+ private String courseId; // 课程编号,如 C001
|
|
|
+ private String courseName; // 课程名称,如 "JavaSE 基础"
|
|
|
+ private int credit; // 学分
|
|
|
+
|
|
|
+ public Course(String courseId, String courseName, int credit) {
|
|
|
+ this.courseId = courseId;
|
|
|
+ this.courseName = courseName;
|
|
|
+ this.credit = credit;
|
|
|
+ }
|
|
|
+ // getter / setter 省略...
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return courseId + "-" + courseName + "-" + credit;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`toString()` 用 `-` 拼接,输出如 `C001-JavaSE 基础-3`,便于菜单展示。
|
|
|
+
|
|
|
+### 学生与成绩 —— 一对多的对象关联
|
|
|
+
|
|
|
+`Student` 中持有一个 `List<Score>`,体现**一对多关系**,并提供操作成绩的方法:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java
|
|
|
+public void addScore(Score s) {
|
|
|
+ scores.add(s);
|
|
|
+}
|
|
|
+
|
|
|
+// 迭代器遍历累加总分(scores 为空返回 0)
|
|
|
+public double getTotalScore() {
|
|
|
+ Iterator<Score> iterator = scores.iterator();
|
|
|
+ double total = 0;
|
|
|
+ while (iterator.hasNext()) {
|
|
|
+ total += iterator.next().getScore();
|
|
|
+ }
|
|
|
+ return total;
|
|
|
+}
|
|
|
+
|
|
|
+public double getAvgScore() {
|
|
|
+ return getTotalScore() / getScoreCount();
|
|
|
+}
|
|
|
+
|
|
|
+public int getScoreCount() {
|
|
|
+ return scores.size();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:总分用 **Iterator 迭代器**遍历,是对 `hasNext()` / `next()` 的复习。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.5 实体层 entity:Score 成绩类 —— setter 校验 + 自定义异常 + equals/hashCode
|
|
|
+
|
|
|
+### setter 中做业务校验并抛自定义异常
|
|
|
+
|
|
|
+成绩必须在 **0~100** 之间,在构造方法中调用 `setScore()` 完成校验:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Score.java
|
|
|
+public class Score {
|
|
|
+ private String studentId; // 学号
|
|
|
+ private String courseId; // 课程编号
|
|
|
+ private double score; // 成绩(0~100)
|
|
|
+
|
|
|
+ public Score(String studentId, String courseId, double score) throws ScoreOutOfBoundsException {
|
|
|
+ this.studentId = studentId;
|
|
|
+ this.courseId = courseId;
|
|
|
+ setScore(score); // 在 setter 中校验并抛出异常
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setScore(double score) throws ScoreOutOfBoundsException {
|
|
|
+ if (score < 0 || score > 100)
|
|
|
+ throw new ScoreOutOfBoundsException("成绩必须在 0~100 之间,当前值:" + score);
|
|
|
+ this.score = score;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:**"构造校验" 设计模式** —— 通过构造方法强制对象创建时就满足数据合法,非法数据根本无法生成对象。
|
|
|
+
|
|
|
+### equals / hashCode —— 以 studentId + courseId 判定同一成绩
|
|
|
+
|
|
|
+同一学生同一课程视为同一条成绩记录,重写 equals 和 hashCode(配合去重 / 集合查找):
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Score.java
|
|
|
+@Override
|
|
|
+public boolean equals(Object o) {
|
|
|
+ if (!(o instanceof Score score)) return false; // instanceof 模式匹配(JDK16+)
|
|
|
+ return Objects.equals(studentId, score.studentId)
|
|
|
+ && Objects.equals(courseId, score.courseId);
|
|
|
+}
|
|
|
+
|
|
|
+@Override
|
|
|
+public int hashCode() {
|
|
|
+ return Objects.hash(studentId, courseId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:重写 equals 后**必须**同步重写 hashCode(保证 hashCode 一致性的约定);`Objects.equals` / `Objects.hash` 是空安全的工具方法;`instanceof` 模式匹配是 JDK16+ 语法。
|
|
|
+
|
|
|
+### Student 的 equals/hashCode —— 以学号判定同一人
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java
|
|
|
+@Override
|
|
|
+public boolean equals(Object o) {
|
|
|
+ if (!(o instanceof Student student)) return false;
|
|
|
+ return Objects.equals(id, student.id);
|
|
|
+}
|
|
|
+
|
|
|
+@Override
|
|
|
+public int hashCode() {
|
|
|
+ return Objects.hashCode(id);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.6 异常层 exception:四个自定义异常类的统一写法
|
|
|
+
|
|
|
+自定义异常**继承 `Exception`**(编译期异常),提供无参构造 + 带 message 构造,写法完全统一:
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/exception/StudentNotFoundException.java
|
|
|
+public class StudentNotFoundException extends Exception {
|
|
|
+ public StudentNotFoundException() { }
|
|
|
+ public StudentNotFoundException(String message) { super(message); }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+四个异常一览:
|
|
|
+
|
|
|
+| 异常类 | 抛出场景 |
|
|
|
+|--------|----------|
|
|
|
+| `DuplicateStudentException` | 添加学生时学号已存在 |
|
|
|
+| `StudentNotFoundException` | 删除 / 查找学生时学号不存在 |
|
|
|
+| `CourseNotFoundException` | 查找 / 删除课程时编号不存在 |
|
|
|
+| `ScoreOutOfBoundsException` | 成绩不在 0~100 范围内 |
|
|
|
+
|
|
|
+> **要点**:继承 `Exception` 属于**编译期(受检)异常**,要求调用方必须 try-catch 或 throws;业务异常用自定义异常携带中文提示信息,`e.getMessage()` 可直接面向用户展示。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.7 管理层 manager:StudentManager(ArrayList + 迭代器 + 增删改查)
|
|
|
+
|
|
|
+### addStudent —— foreach 判重 + 抛自定义异常
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/StudentManager.java
|
|
|
+public class StudentManager {
|
|
|
+ private ArrayList<Student> students; // 泛型集合
|
|
|
+
|
|
|
+ public void addStudent(Student s) throws DuplicateStudentException {
|
|
|
+ // foreach 遍历判重
|
|
|
+ for (Student s1 : students) {
|
|
|
+ if (s1.getId().equals(s.getId())) {
|
|
|
+ throw new DuplicateStudentException("学号已存在:" + s.getId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ students.add(s);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:业务上"学号唯一"的保证方式 —— 先遍历判断(这里用 `getId().equals()` 比对,可改用 `s1.equals(s)` 走重写的 equals)。
|
|
|
+
|
|
|
+### findById —— 必须用 Iterator 迭代器(复习迭代器知识点)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/StudentManager.java
|
|
|
+public Student findById(String id) {
|
|
|
+ Iterator<Student> iterator = students.iterator();
|
|
|
+ while (iterator.hasNext()) {
|
|
|
+ Student student = iterator.next();
|
|
|
+ if (student.getId().equals(id)) {
|
|
|
+ return student; // 找到返回对象
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null; // 找不到返回 null
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### removeById —— 找到即删 + return 截停 + 抛异常
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/StudentManager.java
|
|
|
+public void removeById(String id) throws StudentNotFoundException {
|
|
|
+ for (Student s1 : students) {
|
|
|
+ if (s1.getId().equals(id)) {
|
|
|
+ students.remove(id); // 这里用 remove(Object) 按元素删除
|
|
|
+ return; // 截停:方法立即结束
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new StudentNotFoundException("学生不存在:" + id);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **说明**:`students.remove(id)` 传入的是 String,实际调用的是 `remove(Object)`,会调用 Student 的 `equals` 把"学号相等的学生"删掉(因为 equals 以学号判定),因此**重写 equals 在这里起了作用**。
|
|
|
+
|
|
|
+### updateStudent —— 查出来再改(setter 修改)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/StudentManager.java
|
|
|
+public void updateStudent(String id, String newName, int newAge) {
|
|
|
+ Student student = findById(id);
|
|
|
+ student.setName(newName);
|
|
|
+ student.setAge(newAge);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.8 管理层 manager:CourseManager(Stream + forEach + 方法引用)
|
|
|
+
|
|
|
+### addCourse —— 用"抛异常判断存在性"的反向逻辑
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/CourseManager.java
|
|
|
+public void addCourse(Course c) {
|
|
|
+ try {
|
|
|
+ findById(c.getCourseId()); // 找到 → 说明重复 → 不添加
|
|
|
+ } catch (CourseNotFoundException e) {
|
|
|
+ courses.add(c); // 没找到 → 抛异常 → 才添加
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+public Course findById(String courseId) throws CourseNotFoundException {
|
|
|
+ for (Course c : courses) {
|
|
|
+ if (c.getCourseId().equals(courseId)) {
|
|
|
+ return c;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ throw new CourseNotFoundException();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:这种"以异常控制流程"的写法虽然可行,但**不推荐**(异常开销大、流程不直观);更规范的做法是先判重再 add。
|
|
|
+
|
|
|
+### printAll —— Stream 流 + 方法引用(选做挑战)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/CourseManager.java
|
|
|
+public void printAll() {
|
|
|
+ courses.stream().forEach(System.out::println); // Stream + 方法引用
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`集合.stream()` 获取流 → `forEach` 终结方法遍历 → `System.out::println` 是**引用对象的实例方法**的方法引用形式(等价于 Lambda `c -> System.out.println(c)`)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.9 管理层 manager:ScoreManager(HashMap 按学号分组 + 及格率统计)
|
|
|
+
|
|
|
+### 双数据结构 —— 成绩台账 + 按学号分组 Map
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/ScoreManager.java
|
|
|
+public class ScoreManager {
|
|
|
+ private ArrayList<Score> scores; // 成绩台账(全量)
|
|
|
+ private HashMap<String, List<Score>> scoreMap; // 学号 → 该学生的成绩列表(Map 知识点)
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### addScore —— 三处同步记录 + Map 分组 put 流程
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/ScoreManager.java
|
|
|
+public void addScore(Student stu, Course cou, double sc) throws ScoreOutOfBoundsException {
|
|
|
+ Score score = new Score(stu.getId(), cou.getCourseId(), sc); // 越界抛异常
|
|
|
+ this.scores.add(score); // ① 全局成绩台账
|
|
|
+ stu.addScore(score); // ② 学生侧记录(对象关联)
|
|
|
+ // ③ 更新 scoreMap:按学号分组
|
|
|
+ if (scoreMap.containsKey(stu.getId())) {
|
|
|
+ List<Score> list = scoreMap.get(stu.getId());
|
|
|
+ list.add(score);
|
|
|
+ } else {
|
|
|
+ List<Score> scoreList = new ArrayList<>();
|
|
|
+ scoreMap.put(stu.getId(), scoreList); // 先 put 一个新的 ArrayList
|
|
|
+ scoreList.add(score); // 再 add
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`HashMap<String, List<Score>>` 实现**「学号 → 成绩列表」的分组**;`containsKey` + `get` + `put` 是 Map 的经典三步操作。
|
|
|
+
|
|
|
+### 总分 / 平均分 / 及格率 —— Map 的 keySet 遍历 + 统计
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/manager/ScoreManager.java
|
|
|
+public double getStudentTotal(String studentId) {
|
|
|
+ List<Score> list = scoreMap.get(studentId);
|
|
|
+ double sum = 0;
|
|
|
+ for (Score s : list) {
|
|
|
+ sum += s.getScore();
|
|
|
+ }
|
|
|
+ return sum;
|
|
|
+}
|
|
|
+
|
|
|
+public double getStudentAvg(String studentId) {
|
|
|
+ return getStudentTotal(studentId) / scoreMap.values().size();
|
|
|
+}
|
|
|
+
|
|
|
+// 统计及格(>= 60)率
|
|
|
+public double countPass() {
|
|
|
+ Set<String> studentIds = scoreMap.keySet(); // 拿到所有学生 id
|
|
|
+ int count = 0;
|
|
|
+ for (String studentId : studentIds) {
|
|
|
+ double studentAvg = getStudentAvg(studentId);
|
|
|
+ if (studentAvg >= 60) {
|
|
|
+ count++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return count / studentIds.size();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`keySet()` 拿全部键、`values()` 拿全部值、`size()` 取个数;及格率 = 及格人数 ÷ 总人数(结果建议再乘 100 保留两位小数展示)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.10 工具层 util:RankUtil —— Comparable / Comparator / Lambda 三种排序方式
|
|
|
+
|
|
|
+这一层是**排序知识点集大成**:一个工具类演示了四种排序写法。
|
|
|
+
|
|
|
+### ① TreeSet 自然排序 —— 依赖 Student 的 compareTo(平均分降序 + 学号二次比较)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/util/RankUtil.java
|
|
|
+public static void showRank(StudentManager sm) {
|
|
|
+ List<Student> studentList = sm.getStudents();
|
|
|
+ TreeSet<Student> treeSet = new TreeSet<>(studentList); // 自动自然排序 + 去重
|
|
|
+ Iterator<Student> iterator = treeSet.iterator();
|
|
|
+ while (iterator.hasNext()) {
|
|
|
+ System.out.println(iterator.next());
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/entity/Student.java(Comparable 实现)
|
|
|
+@Override
|
|
|
+public int compareTo(Student o) {
|
|
|
+ // 按平均分【降序】:交换参数位置即可实现降序
|
|
|
+ int result = Double.compare(o.getAvgScore(), this.getAvgScore());
|
|
|
+ // 平均分相同再按学号【升序】二次比较
|
|
|
+ return result == 0 ? this.id.compareTo(o.getId()) : result;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`Comparable` 自然排序规则写在实体类内部;`Double.compare` 对 double 比较更安全(避免差值溢出);**降序技巧 = 交换 compareTo 两个参数的先后位置**;`TreeSet` 会**自动排序 + 按 compareTo 去重**。
|
|
|
+
|
|
|
+### ② Comparator 匿名内部类 —— 按姓名升序
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/util/RankUtil.java
|
|
|
+public static void sortByName(StudentManager sm) {
|
|
|
+ List<Student> studentList = sm.getStudents();
|
|
|
+ Collections.sort(studentList, new Comparator<Student>() {
|
|
|
+ @Override
|
|
|
+ public int compare(Student o1, Student o2) {
|
|
|
+ return o1.getName().compareTo(o2.getName());
|
|
|
+ }
|
|
|
+ });
|
|
|
+ sm.printAll();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`Comparator` 比较器排序规则写在**类外部**(作为排序参数传入),不改实体类即可临时指定排序规则;String 的 `compareTo` 按字典序比较。
|
|
|
+
|
|
|
+### ③ Comparator + Lambda —— 按年龄升序(Lambda 省略写法)
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/util/RankUtil.java
|
|
|
+public static void sortByAge(StudentManager sm) {
|
|
|
+ List<Student> studentList = sm.getStudents();
|
|
|
+ Collections.sort(studentList, (s1, s2) -> Integer.compare(s1.getAge(), s2.getAge()));
|
|
|
+ sm.printAll();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:`Comparator` 是**函数式接口**(只有一个抽象方法 `compare`),可以用 Lambda 替代匿名内部类;`Integer.compare` 是 int 比较的安全写法。
|
|
|
+
|
|
|
+### ④ Comparator 匿名内部类 —— 不依赖 compareTo,自己按平均分降序
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/util/RankUtil.java
|
|
|
+public static void sortByAvgDesc(StudentManager sm) {
|
|
|
+ List<Student> studentList = sm.getStudents();
|
|
|
+ Collections.sort(studentList, new Comparator<Student>() {
|
|
|
+ @Override
|
|
|
+ public int compare(Student o1, Student o2) {
|
|
|
+ return Double.compare(o2.getAvgScore(), o1.getAvgScore()); // 降序:交换参数
|
|
|
+ }
|
|
|
+ });
|
|
|
+ sm.printAll();
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:与 ① 的区别 —— 排序规则不依赖 Student 内部实现的 compareTo,而是**在 Comparator 中自己计算**,体现了 Comparable 与 Comparator 的取舍。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 3.11 入口 main:StudentSystemApp —— Scanner + while + switch + 标签 break 菜单系统
|
|
|
+
|
|
|
+### 初始化示例数据 + 代码块
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/main/StudentSystemApp.java
|
|
|
+public static void main(String[] args) {
|
|
|
+ // 1. 创建各管理类对象
|
|
|
+ StudentManager sm = new StudentManager();
|
|
|
+ CourseManager cm = new CourseManager();
|
|
|
+ ScoreManager scm = new ScoreManager();
|
|
|
+
|
|
|
+ // 2. 初始化少量示例数据
|
|
|
+ Student s1 = new Student("张三", 18, "S001", "Java2301");
|
|
|
+ Student s2 = new Student("李四", 19, "S002", "Java2301");
|
|
|
+ Student s3 = new Student("王五", 20, "S003", "Java2302");
|
|
|
+ Course c1 = new Course("C001", "JavaSE 基础", 3);
|
|
|
+ Course c2 = new Course("C002", "MySQL 数据库", 2);
|
|
|
+ Course c3 = new Course("C003", "数据结构", 3);
|
|
|
+ Teacher teacher = new Teacher("王老师", 35, "T001", c1);
|
|
|
+
|
|
|
+ { // 普通代码块:限定 try-catch 作用域
|
|
|
+ try {
|
|
|
+ sm.addStudent(s1);
|
|
|
+ sm.addStudent(s2);
|
|
|
+ sm.addStudent(s3);
|
|
|
+ cm.addCourse(c1);
|
|
|
+ cm.addCourse(c2);
|
|
|
+ cm.addCourse(c3);
|
|
|
+ } catch (DuplicateStudentException e) {
|
|
|
+ throw new RuntimeException(e); // 编译期异常包装成运行时异常
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:用**普通代码块**(`{ }`)把初始化 try-catch 的局部变量作用域圈起来;`throw new RuntimeException(e)` 是把受检异常包装为运行时异常的一种方式(异常链)。
|
|
|
+
|
|
|
+### 主循环 + 主菜单 + 标签 break
|
|
|
+
|
|
|
+```java
|
|
|
+// 来源:homework0804/main/StudentSystemApp.java
|
|
|
+Scanner sc = new Scanner(System.in);
|
|
|
+
|
|
|
+// 主循环 + 主菜单(while + switch 箭头语法)
|
|
|
+one:
|
|
|
+while (true) {
|
|
|
+ System.out.println("===== 学生信息管理系统 =====");
|
|
|
+ System.out.println("1. 学生管理");
|
|
|
+ System.out.println("2. 课程管理");
|
|
|
+ System.out.println("3. 成绩管理");
|
|
|
+ System.out.println("4. 成绩排名");
|
|
|
+ System.out.println("5. 高级查询(挑战·选做)");
|
|
|
+ System.out.println("0. 退出");
|
|
|
+ switch (sc.next()) {
|
|
|
+ case "1" -> {
|
|
|
+ two:
|
|
|
+ while (true) {
|
|
|
+ // 二级菜单:学生管理
|
|
|
+ switch (sc.next()) {
|
|
|
+ case "1" -> { // 添加学生
|
|
|
+ // Scanner 接收 name/age/id/clazz → new Student → sm.addStudent
|
|
|
+ try {
|
|
|
+ sm.addStudent(student);
|
|
|
+ } catch (DuplicateStudentException e) {
|
|
|
+ System.out.println(e.getMessage()); // 界面友好提示
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "2" -> { // 删除学生
|
|
|
+ try {
|
|
|
+ sm.removeById(id);
|
|
|
+ } catch (StudentNotFoundException e) {
|
|
|
+ System.out.println(e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "3" -> { // 查找学生
|
|
|
+ Student student = sm.findById(id);
|
|
|
+ System.out.println(student);
|
|
|
+ }
|
|
|
+ case "4" -> { // 查看所有学生
|
|
|
+ for (Student student : sm.getStudents()) {
|
|
|
+ System.out.println(student);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "0" -> {
|
|
|
+ System.out.println("返回上一级");
|
|
|
+ break two; // 标签 break:跳出外层二级循环
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case "2" -> { } // 课程管理(待完成)
|
|
|
+ case "3" -> { } // 成绩管理(待完成)
|
|
|
+ case "4" -> { } // 成绩排名(待完成)
|
|
|
+ case "5" -> { } // 高级查询(待完成)
|
|
|
+ case "0" -> { }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+> **要点**:
|
|
|
+> - **switch 箭头语法**(JDK14+):`case "1" -> { ... }`,无需 break,代码更简洁
|
|
|
+> - **标签 break**:`two:` 给 while 循环命名,`break two` 可以直接跳出**外层二级循环**(普通 break 只能跳一层)
|
|
|
+> - 每个业务功能都用 **try-catch 处理自定义异常**并输出 `e.getMessage()`,让程序**鲁棒**(不因异常崩溃,而是提示用户)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 知识点全景总结
|
|
|
+
|
|
|
+本作业把 JavaSE 阶段核心知识点**串成一个系统**,一张表看清"知识 → 代码位置":
|
|
|
+
|
|
|
+| 知识点 | 具体体现 | 源码位置 |
|
|
|
+|--------|----------|----------|
|
|
|
+| **类与对象 / 封装** | 私有属性 + getter/setter + 构造方法 | entity 下所有实体类 |
|
|
|
+| **继承** | `Student extends Person` / `Teacher extends Person`,`super(name, age)` | entity/Student、Teacher、Person |
|
|
|
+| **抽象类** | `abstract class Person` + 抽象方法 `getRole()` | entity/Person |
|
|
|
+| **接口** | `Studyable` / `Teachable`,多接口实现 | entity/Student、Teacher |
|
|
|
+| **对象关联** | Teacher 持有 Course;Student 持有 `List<Score>` | entity/Teacher、Student |
|
|
|
+| **自定义异常** | 4 个异常类继承 Exception | exception/ |
|
|
|
+| **异常处理** | throws 声明 + try-catch 捕获 + getMessage 提示 | manager、main |
|
|
|
+| **集合框架** | `ArrayList`(学生/课程/成绩)、`HashMap`(学号分组)、`TreeSet`(排名) | manager、util |
|
|
|
+| **迭代器** | `Iterator` 遍历(hasNext/next) | Student、StudentManager |
|
|
|
+| **比较器排序** | Comparable(compareTo)+ Comparator(匿名内部类)+ Lambda + Double.compare | entity/Student、util/RankUtil |
|
|
|
+| **equals/hashCode** | Student 按学号、Score 按 学号+课程 重写 | entity/Student、Score |
|
|
|
+| **Stream 流(选做)** | `stream().forEach(System.out::println)` 方法引用 | CourseManager、StudentManager |
|
|
|
+| **流程控制** | Scanner 输入 + while 主循环 + switch 箭头语法 + 标签 break | main/StudentSystemApp |
|
|
|
+| **代码块** | 普通代码块 `{ }` 限定初始化 try-catch 作用域 | main/StudentSystemApp |
|
|
|
+| **IO 流(字节流)** | IO 概念、输入/输出与字节/字符流分类、OutputStream/InputStream 抽象父类、FileOutputStream 字节输出流三步走(创建流对象 → write() 写入 → close() 关闭)、四种构造方法、write(int/byte[]/byte[]+off+len) 三种写法、append 追加模式、getBytes() 字符串转字节、换行符 | course/Demo02、course/FileOutputStreamTest |
|
|
|
+| **字节输入流(IO)** | FileInputStream 读取三步走(创建 → read() 读 → close())、read()/read(byte[])/read(byte[]+off+len) 三种读法、read 返回 -1 表示 EOF、read(byte[]) 返回有效字节数、new String(bytes,0,length,UTF-8) 转字符串、try-catch-finally 关闭资源;文件复制 FileCopy(输入流读 + 输出流写 + byte[] 缓冲区循环 + 耗时统计) | course/Demo02、course/FileInputStreamTest、course/FileCopy |
|
|
|
+| **文件操作 File** | File 三种构造、`createNewFile()`/`mkdir()`/`mkdirs()`/`delete()`、`isFile()`/`isDirectory()`/`exists()`、`getAbsolutePath()`/`getName()`/`listFiles()`、绝对/相对路径与分隔符、IOException | course/Demo01 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 随堂练习要点
|
|
|
+
|
|
|
+- 完成 `StudentSystemApp` 中**尚未实现**的菜单分支:课程管理、成绩管理、成绩排名、高级查询
|
|
|
+- 思考并改进 `CourseManager.addCourse` —— 用「try-catch 控制流程」的写法有什么问题?如何用 `contains` / 布尔标志改写?
|
|
|
+- 尝试在 `ScoreManager.countPass` 中把及格率输出为百分数(`* 100` 并保留两位小数)
|
|
|
+- 挑战 · 选做:用 **Stream 流**实现「高级查询」,如筛选出平均分 >= 85 的学生姓名列表(`filter + map + collect(Collectors.toList())`)
|
|
|
+- 比较 `RankUtil` 四种排序写法(TreeSet 自然排序 / 匿名内部类 / Lambda)各自的使用场景与优缺点
|
|
|
+- IO 流练习:用 `FileOutputStream` 往文件中写入一行字符串(先 `getBytes()` 转字节数组),再用 `append=true` 追加第二行,验证追加 vs 覆盖的区别;注意写完后 `close()` 关闭流
|
|
|
+- IO 流练习:用 `FileInputStream` 读取刚写入的文件并输出到控制台(`while ((len = fis.read(bytes)) != -1)` 循环读 + UTF-8 转字符串);再封装一个文件复制方法(输入流读 + 输出流写 + `byte[4096]` 缓冲区),复制一个视频文件并对比缓冲区 1024 / 4096 大小下的复制耗时
|
|
|
+- File 练习:分别用 `new File("a.txt")`、`new File("a", "b.txt")`、`new File(new File("a"), "b.txt")` 三种构造创建对象,并输出 `getAbsolutePath()` 对比路径差异;用 `mkdirs()` 创建多级目录后用 `listFiles()` 遍历查看;尝试 `createNewFile()` 并捕获 `IOException`
|
|
|
+- File 练习:思考 `mkdir()` 与 `mkdirs()` 在"父目录不存在"时的行为差异;`listFiles()` 返回的 `File[]` 数组如何用 foreach 取出每个子项的名字
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 拓展阅读
|
|
|
+
|
|
|
+- **JDK 文档**:[`java.util.Comparator`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/Comparator.html)、[`java.lang.Comparable`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Comparable.html)、[`java.io.File`](https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/io/File.html)
|
|
|
+- **接口默认方法**:若在 `Studyable` 中新增 `default` 方法,对已有实现类是否有影响?(接口演进问题)
|
|
|
+- **异常链**:`throw new RuntimeException(e)` 与 `catch (DuplicateStudentException e) { throw new RuntimeException(e); }` 的区别
|
|
|
+- **HashMap 分组**:`computeIfAbsent(key, k -> new ArrayList<>()).add(score)` 可以一行替代本作业中 `containsKey + put + get` 三步写法
|
|
|
+- **equals 契约**:为什么重写 equals 必须重写 hashCode?(HashMap/TreeSet 去重与查找的正确性)
|
|
|
+- **分层设计**:本作业的 entity/manager/main 分层是后续**三层架构**(Controller/Service/DAO)与 **MVC** 的雏形
|
|
|
+- **File 与 IO 流的关系**:File 只管理路径,真正的文件读写要用 **IO 流**(`FileInputStream` / `FileOutputStream` / `FileReader` / `FileWriter`),File 常作为流构造方法的参数传入,下一课将进入 IO 流学习
|
|
|
+- **路径分隔符的跨平台问题**:硬编码 `\` 在 Linux 上会失效,推荐用 `File.separator` 或 `File.pathSeparator` 获取系统对应的分隔符
|
|
|
+- **IO 流进阶方向**:本日学了字节流的读写(FileOutputStream 写 / FileInputStream 读 / FileCopy 复制),后续会学字符流(Reader/Writer,适合纯文本避免乱码)、缓冲流(BufferedInputStream/BufferedOutputStream 效率更高)、转换流,以及 JDK7+ 的 **try-with-resources**(`try (FileInputStream fis = new FileInputStream(...)) { }` 可自动 close 释放资源,替代手写 finally close)
|
|
|
+
|