# 20260806 课堂笔记 — File 文件操作与 IO 流入门 - **日期**: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` 和 `Studyable` 两个接口,**「继承 + 多接口」**是 Java 多继承能力的替代方案: ```java // 来源:homework0804/entity/Student.java public class Student extends Person implements Comparable, Studyable { private String id; // 学号,如 S001 private String clazz; // 班级,如 "Java2301" private List 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`,体现**一对多关系**,并提供操作成绩的方法: ```java // 来源:homework0804/entity/Student.java public void addScore(Score s) { scores.add(s); } // 迭代器遍历累加总分(scores 为空返回 0) public double getTotalScore() { Iterator 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 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 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 scores; // 成绩台账(全量) private HashMap> 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 list = scoreMap.get(stu.getId()); list.add(score); } else { List scoreList = new ArrayList<>(); scoreMap.put(stu.getId(), scoreList); // 先 put 一个新的 ArrayList scoreList.add(score); // 再 add } } ``` > **要点**:`HashMap>` 实现**「学号 → 成绩列表」的分组**;`containsKey` + `get` + `put` 是 Map 的经典三步操作。 ### 总分 / 平均分 / 及格率 —— Map 的 keySet 遍历 + 统计 ```java // 来源:homework0804/manager/ScoreManager.java public double getStudentTotal(String studentId) { List 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 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 studentList = sm.getStudents(); TreeSet treeSet = new TreeSet<>(studentList); // 自动自然排序 + 去重 Iterator 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 studentList = sm.getStudents(); Collections.sort(studentList, new Comparator() { @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 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 studentList = sm.getStudents(); Collections.sort(studentList, new Comparator() { @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` | 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)