20260806-笔记.md 47 KB

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 的构造 / 创建 / 删除 / 判断 / 获取
  2. IO 流入门:字节流(FileOutputStream 输出 / FileInputStream 输入 / 文件复制)
  3. JavaSE 综合大作业(20260804 课后作业)解析:学生信息管理系统
  4. 知识点全景总结
  5. 随堂练习要点
  6. 拓展阅读

1. 文件操作类 File:java.io.File 的构造 / 创建 / 删除 / 判断 / 获取

概念 —— 文件与目录的抽象表示

java.io.File 是文件和目录路径名的抽象表示:文件(File)和目录(文件夹)都可以封装成 File 对象。注意:File 封装的不一定是真实存在的文件,只是一个路径名——它可以是存在的,也可以是不存在的,需要将来通过这个路径进行具体操作,把文件路径内容转换为真实存在的。

// 来源:course/Demo01.java(注释部分)
/*
    java.io.File
    File类,是文件和目录路径名的抽象表示(类)
    文件和目录(文件夹)可以通过File封装成对象
    对于File来说,他的封装并不是一个真正存在的文件,只是一个路径名,他是可以存在,也可以不存在,
    需要将来通过这个路径进行具体的操作,把文件路径内容转换为真实存在的。
*/

要点:File 类位于 java.io 包(IO 入门第一课),它只描述路径、不做真正的读写;真正读写文件内容需要后续的 IO 流(InputStream/OutputStream/Reader/Writer)。

三种构造方法

// 来源: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(可理解为快递地址/经纬度)
相对路径 简化的路径,相对于某个位置(可理解为前后左右),如 ./ 表示当前项目的同一层级,../ 表示当前项目的上一层级

创建 / 删除 / 判断 / 获取 功能方法

// 来源: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 演示 —— 相对路径操作 + 六大获取/判断方法

// 来源: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 流用来处理设备之间的数据传输问题,常见应用场景:文件复制、文件上传、文件下载。

// 来源:course/Demo02.java(注释部分)
/*
    IO流
    IO:输入输出(Input/Output)
    流:Stream是一种抽象概念,是对数据传输的总称,也就是说数据在设备直接的传输称为流,流的本质就是数据传输
    IO流就是用来处理设备之间的数据传输问题的,常见应用场景:文件赋值、文件上传、文件下载。
*/

IO 流的分类(两个维度)

分类维度 类别 说明
按数据流向(相对于内存) 输入流 向内存输入数据(读数据)
输出流 从内存往外输出数据(写数据)
按数据类型 字节流 传输的每个数据都是 byte 类型(字节输入流 / 字节输出流)
字符流 传输的每个数据都是 char 类型(字符输入流 / 字符输出流)

IO 流的使用场景推荐

  • 纯文本文件(.txt):建议使用字符流,因为 1 个字符等于 2 个字节
  • 除纯文本之外的任何文件(Word / Excel / PPT / md / png / mp3 / mp4 / rmvb 等):建议字节流
  • 不确定文件类型:用字节流

    // 来源:course/Demo02.java(注释部分)
    /*
    如果是纯文本文件(.txt),建议使用字符流,因为1个字符等于2个字节
    除了纯文本文件之外的任何文件包括文本、图片、视频、音频、等等任何文件,都建议是字节流。
    如果不确定文件类型,就是有字节流
    */
    

InputStream 与 OutputStream —— 字节流两大抽象父类

  • OutputStream:抽象类,表示所有字节输出流类的父类
  • InputStream:抽象类,表示所有字节输入流类的父类
  • 子类命名特点:子类的名称都以父类类名作为子类的后缀(如文件字节输入流 FileInputStream、文件字节输出流 FileOutputStream)

    // 来源:course/Demo02.java(注释部分)
    /*
    OutputStream:这是一个抽象类,表示所有字节输出流类的父类
    InputStream:这是一个抽象类,表示所有字节输入流类的父类
    文件字节输入流:FileInputStream
    文件字节输出流:FileOutputStream
    */
    

使用字节输出流的三个步骤

  1. 创建字节输出流对象
  2. 调用字节输出流的写数据方法
  3. 释放资源,关闭流

    // 来源:course/Demo02.java(注释部分)
    /*
    使用字节输出流的步骤:
        1、创建字节输出流对象
        2、调用字节输出流的写数据的方法
        3、释放资源,关闭流
    */
    

FileOutputStream 的四种构造方法

// 来源:course/FileOutputStreamTest.java(注释部分)
FileOutputStream(File file)                   // 创建输出流,参数为 File 对象
FileOutputStream(String name)                 // 创建输出流,参数为字符串路径
FileOutputStream(File file, boolean append)   // append 为 true 表示追加内容(否则覆盖)
FileOutputStream(String name, boolean append) // 字符串路径 + 追加开关

write() 三种写数据方法

// 来源:course/FileOutputStreamTest.java(注释部分)
write(int b)                 // 写一个字节
write(byte[] b)              // 写一个字节数组的内容
write(byte[] b, int off, int len)   // 写字节数组的内容,从 off 开始,长度为 len

main 演示 —— 三步走写入文件

// 来源: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 都可能抛编译期异常

使用字节输入流的步骤(与输出流对称)

// 来源: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) 返回本次读取到的有效字节长度。

// 来源: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 把缓冲区写入目标文件,循环直到读完。

// 来源: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();
}
// 来源: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(排序)
// 来源: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() 强制子类返回角色描述:

// 来源: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 多继承能力的替代方案:

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

// 来源:homework0804/entity/Student.java
@Override
public String getRole() {
    return "学生";
}

要点:抽象类不能被实例化,子类必须重写所有抽象方法(否则子类也必须是抽象类)。


3.3 实体层 entity:接口 Studyable / Teachable 与多接口实现

接口定义 —— 规范"能做什么"

// 来源:homework0804/entity/Studyable.java
public interface Studyable {
    void study();   // 学习
}
// 来源:homework0804/entity/Teachable.java
public interface Teachable {
    void teach();   // 授课
}

接口实现

Student 实现 Studyable,Teacher 实现 Teachable:

// 来源:homework0804/entity/Student.java
@Override
public void study() {
    System.out.println(this.getName() + " 同学正在努力学习...");
}
// 来源: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 对象,这就是对象关联 / 对象组合:

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

// 来源: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>,体现一对多关系,并提供操作成绩的方法:

// 来源: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() 完成校验:

// 来源: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(配合去重 / 集合查找):

// 来源: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 —— 以学号判定同一人

// 来源: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 构造,写法完全统一:

// 来源: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 判重 + 抛自定义异常

// 来源: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 迭代器(复习迭代器知识点)

// 来源: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 截停 + 抛异常

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

// 来源: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 —— 用"抛异常判断存在性"的反向逻辑

// 来源: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 流 + 方法引用(选做挑战)

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

// 来源:homework0804/manager/ScoreManager.java
public class ScoreManager {
    private ArrayList<Score> scores;                    // 成绩台账(全量)
    private HashMap<String, List<Score>> scoreMap;      // 学号 → 该学生的成绩列表(Map 知识点)
}

addScore —— 三处同步记录 + Map 分组 put 流程

// 来源: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 遍历 + 统计

// 来源: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(平均分降序 + 学号二次比较)

// 来源: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());
    }
}
// 来源: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 匿名内部类 —— 按姓名升序

// 来源: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 省略写法)

// 来源: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,自己按平均分降序

// 来源: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 菜单系统

初始化示例数据 + 代码块

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

// 来源: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、java.lang.Comparable、java.io.File
  • 接口默认方法:若在 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)