c260806course / homework0804(entity / exception / manager / main / util 分层)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对象)和子路径名字符串创建
| 概念 | 说明 |
|---|---|
| 路径分隔符 | 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() // 返回该路径目录下所有文件和目录组成的数组
// 来源: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()vsmkdir():前者多级递归创建父目录,更常用listFiles()返回File[]数组,用Arrays.toString()打印可看到目录下所有文件/子目录./相对路径指向当前项目目录,是后续文件读写的起点概念
IO 是输入输出(Input/Output);流(Stream)是一种抽象概念,是对数据传输的总称——数据在设备之间的传输称为流,流的本质就是数据传输。IO 流用来处理设备之间的数据传输问题,常见应用场景:文件复制、文件上传、文件下载。
// 来源:course/Demo02.java(注释部分)
/*
IO流
IO:输入输出(Input/Output)
流:Stream是一种抽象概念,是对数据传输的总称,也就是说数据在设备直接的传输称为流,流的本质就是数据传输
IO流就是用来处理设备之间的数据传输问题的,常见应用场景:文件赋值、文件上传、文件下载。
*/
| 分类维度 | 类别 | 说明 |
|---|---|---|
| 按数据流向(相对于内存) | 输入流 | 向内存输入数据(读数据) |
| 输出流 | 从内存往外输出数据(写数据) | |
| 按数据类型 | 字节流 | 传输的每个数据都是 byte 类型(字节输入流 / 字节输出流) |
| 字符流 | 传输的每个数据都是 char 类型(字符输入流 / 字符输出流) |
不确定文件类型:用字节流
// 来源:course/Demo02.java(注释部分)
/*
如果是纯文本文件(.txt),建议使用字符流,因为1个字符等于2个字节
除了纯文本文件之外的任何文件包括文本、图片、视频、音频、等等任何文件,都建议是字节流。
如果不确定文件类型,就是有字节流
*/
OutputStream:抽象类,表示所有字节输出流类的父类InputStream:抽象类,表示所有字节输入流类的父类子类命名特点:子类的名称都以父类类名作为子类的后缀(如文件字节输入流 FileInputStream、文件字节输出流 FileOutputStream)
// 来源:course/Demo02.java(注释部分)
/*
OutputStream:这是一个抽象类,表示所有字节输出流类的父类
InputStream:这是一个抽象类,表示所有字节输入流类的父类
文件字节输入流:FileInputStream
文件字节输出流:FileOutputStream
*/
释放资源,关闭流
// 来源:course/Demo02.java(注释部分)
/*
使用字节输出流的步骤:
1、创建字节输出流对象
2、调用字节输出流的写数据的方法
3、释放资源,关闭流
*/
// 来源:course/FileOutputStreamTest.java(注释部分)
FileOutputStream(File file) // 创建输出流,参数为 File 对象
FileOutputStream(String name) // 创建输出流,参数为字符串路径
FileOutputStream(File file, boolean append) // append 为 true 表示追加内容(否则覆盖)
FileOutputStream(String name, boolean append) // 字符串路径 + 追加开关
// 来源:course/FileOutputStreamTest.java(注释部分)
write(int b) // 写一个字节
write(byte[] b) // 写一个字节数组的内容
write(byte[] b, int off, int len) // 写字节数组的内容,从 off 开始,长度为 len
// 来源: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、释放资源,关闭流
*/
读取方法(与 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()效率高得多
文件复制 = 输入流读 + 输出流写:用 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 起的毫秒数,两时刻相减即可统计复制耗时
这是 20260804 课后作业 —— JavaSE 综合大作业,把整个 JavaSE 阶段学过的核心知识点综合到一个小型"学生信息管理系统"中,核心功能:
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)的雏形。
把"学生 / 教师"的公共属性(姓名、年龄)抽到父类 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 继承 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<>();
}
}
// 来源:homework0804/entity/Student.java
@Override
public String getRole() {
return "学生";
}
要点:抽象类不能被实例化,子类必须重写所有抽象方法(否则子类也必须是抽象类)。
// 来源: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重写;接口是"契约",体现面向接口编程思想。
一个教师"教一门课",所以 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 只含属性 + 构造方法 + 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()的复习。
成绩必须在 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(配合去重 / 集合查找):
// 来源: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+ 语法。
// 来源: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);
}
自定义异常继承 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()可直接面向用户展示。
// 来源: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)。
// 来源: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
}
// 来源: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 在这里起了作用。
// 来源:homework0804/manager/StudentManager.java
public void updateStudent(String id, String newName, int newAge) {
Student student = findById(id);
student.setName(newName);
student.setAge(newAge);
}
// 来源: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。
// 来源:homework0804/manager/CourseManager.java
public void printAll() {
courses.stream().forEach(System.out::println); // Stream + 方法引用
}
要点:
集合.stream()获取流 →forEach终结方法遍历 →System.out::println是引用对象的实例方法的方法引用形式(等价于 Lambdac -> System.out.println(c))。
// 来源:homework0804/manager/ScoreManager.java
public class ScoreManager {
private ArrayList<Score> scores; // 成绩台账(全量)
private HashMap<String, List<Score>> scoreMap; // 学号 → 该学生的成绩列表(Map 知识点)
}
// 来源: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 的经典三步操作。
// 来源: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 保留两位小数展示)。
这一层是排序知识点集大成:一个工具类演示了四种排序写法。
// 来源: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 去重。
// 来源: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按字典序比较。
// 来源: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 比较的安全写法。
// 来源: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 的取舍。
// 来源: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)是把受检异常包装为运行时异常的一种方式(异常链)。
// 来源: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(),让程序鲁棒(不因异常崩溃,而是提示用户)
本作业把 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 |
StudentSystemApp 中尚未实现的菜单分支:课程管理、成绩管理、成绩排名、高级查询CourseManager.addCourse —— 用「try-catch 控制流程」的写法有什么问题?如何用 contains / 布尔标志改写?ScoreManager.countPass 中把及格率输出为百分数(* 100 并保留两位小数)filter + map + collect(Collectors.toList()))RankUtil 四种排序写法(TreeSet 自然排序 / 匿名内部类 / Lambda)各自的使用场景与优缺点FileOutputStream 往文件中写入一行字符串(先 getBytes() 转字节数组),再用 append=true 追加第二行,验证追加 vs 覆盖的区别;注意写完后 close() 关闭流FileInputStream 读取刚写入的文件并输出到控制台(while ((len = fis.read(bytes)) != -1) 循环读 + UTF-8 转字符串);再封装一个文件复制方法(输入流读 + 输出流写 + byte[4096] 缓冲区),复制一个视频文件并对比缓冲区 1024 / 4096 大小下的复制耗时new File("a.txt")、new File("a", "b.txt")、new File(new File("a"), "b.txt") 三种构造创建对象,并输出 getAbsolutePath() 对比路径差异;用 mkdirs() 创建多级目录后用 listFiles() 遍历查看;尝试 createNewFile() 并捕获 IOExceptionmkdir() 与 mkdirs() 在"父目录不存在"时的行为差异;listFiles() 返回的 File[] 数组如何用 foreach 取出每个子项的名字java.util.Comparator、java.lang.Comparable、java.io.FileStudyable 中新增 default 方法,对已有实现类是否有影响?(接口演进问题)throw new RuntimeException(e) 与 catch (DuplicateStudentException e) { throw new RuntimeException(e); } 的区别computeIfAbsent(key, k -> new ArrayList<>()).add(score) 可以一行替代本作业中 containsKey + put + get 三步写法FileInputStream / FileOutputStream / FileReader / FileWriter),File 常作为流构造方法的参数传入,下一课将进入 IO 流学习\ 在 Linux 上会失效,推荐用 File.separator 或 File.pathSeparator 获取系统对应的分隔符try (FileInputStream fis = new FileInputStream(...)) { } 可自动 close 释放资源,替代手写 finally close)