ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java面向对象实验代码全解析:从类到异常处理

Java面向对象实验代码全解析:从类到异常处理

简介:这份资源是东北大学Java面向对象实验课程第1至6次的完整代码合集,面向正在学习Java OOP的高校学生与自学者,帮助解决实验无从下手、缺少参考实现的问题。压缩包共124个文件,约51KB,以java源码与class字节码为主,辅以html说明页面、dat数据文件、xml配置及少量gif与jar,覆盖类与对象、封装、继承、多态、构造器、接口、异常处理、集合框架、文件流、数据结构与设计模式等核心知识点,从内容预览可见还包含GourmetCoffeeGUI等图形界面与目录加载相关类,实验场景较为完整。目前已有2002人学习下载,说明该套代码在课程学习者中认可度较高。读者可据此对照实验要求梳理类结构与方法调用关系,理解私有变量与公共接口的封装思路、子类对父类的扩展方式以及接口实现与多态调用,并参考异常捕获与集合存储的写法,快速搭建可运行工程、排查编译与运行错误,为后续复杂Java应用开发打下扎实基础。

1. 从六份 Java 实验代码里,我拆出了面向对象最该练的几件事

带过几轮 Java 实验的人都有个共识:面向对象这东西,光看语法是学不会的,必须动手把类、继承、多态、接口、集合、异常这些点一个个敲进代码里,才能形成肌肉记忆。这份「Java 面向对象实验全部实验代码 1 到 6」就是一套按实验顺序排好的完整代码包,覆盖了从类与对象基础、封装继承、多态与抽象类、接口与内部类,到集合框架、异常处理与文件 IO 的典型练习。它解决的不是「教你 Java 语法」的问题,而是「给你一套能跑、能改、能对照的参考实现」,让你在做实验卡壳时有个能翻的底本。适合正在上 Java 面向对象课、需要交实验报告的学生,也适合想系统补一遍 OOP 基本功的转行者。下面我按实验顺序拆开讲,重点放在每个实验的设计意图、代码结构、参数怎么调,以及我踩过的那些坑。

2. 实验 1 到 3:类、封装、继承的代码骨架怎么搭

前三个实验是整个面向对象的地基,很多人觉得简单就跳过,结果后面写多态和接口时连构造方法链都理不清。这三个实验的代码量不大,但每一行都在训练「把现实问题抽象成类」的思维。

2.1 实验 1:类与对象的最小闭环

实验 1 通常要求定义一个简单的类,比如学生类或矩形类,包含私有属性、构造方法、getter/setter,再在 main 里创建对象并调用方法。这份代码里用的是「学生成绩管理」的场景,核心结构如下:

// Student.java public class Student { private String name; // 姓名,私有属性 private int score; // 成绩,私有属性 // 无参构造 public Student() { this("未知", 0); // 调用有参构造,避免重复赋值 } // 有参构造 public Student(String name, int score) { this.name = name; this.score = score; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getScore() { return score; } // 设置成绩时做合法性校验 public void setScore(int score) { if (score < 0 || score > 100) { throw new IllegalArgumentException("成绩必须在 0-100 之间"); } this.score = score; } @Override public String toString() { return "Student{name='" + name + "', score=" + score + "}"; } }

这段代码的关键点有三个:一是this("未知", 0)这种构造方法链,能减少重复赋值逻辑;二是setScore里加了范围校验,这是封装的意义——不让外部随意改坏内部状态;三是重写了toString(),方便打印调试。参数方面,score用int而不是String,是为了后续排序和比较方便,如果你实验里要求成绩带小数,改成double即可,但要注意浮点数比较的精度问题。

2.2 实验 2:继承与方法重写

实验 2 一般会引入继承关系,比如让GraduateStudent继承Student,增加导师、研究方向等字段。这份代码里的做法是:

// GraduateStudent.java public class GraduateStudent extends Student { private String supervisor; // 导师 private String researchArea; // 研究方向 public GraduateStudent(String name, int score, String supervisor, String researchArea) { super(name, score); // 必须先调用父类构造 this.supervisor = supervisor; this.researchArea = researchArea; } @Override public String toString() { return super.toString() + ", supervisor='" + supervisor + "'"; } }

这里最容易翻车的地方是super(name, score)的位置——它必须是子类构造方法的第一条语句,否则编译不过。另外,如果父类没有无参构造,子类就必须显式调用super(...),这个规则在实验 3 里会反复出现。参数上,supervisor和researchArea用String存储,如果实验要求关联到某个导师对象,那就得再定义一个Teacher类,把这里改成Teacher supervisor,这就是从「字符串建模」到「对象建模」的升级。

2.3 实验 3:多态与抽象类的落地

实验 3 通常要求把父类改成抽象类,或者定义抽象方法,让不同子类给出不同实现。这份代码里把Student改成了抽象类,增加了一个抽象方法getDescription():

// Student.java 改为抽象类 public abstract class Student { protected String name; protected int score; public Student(String name, int score) { this.name = name; this.score = score; } // 抽象方法,子类必须实现 public abstract String getDescription(); public int getScore() { return score; } }

然后在GraduateStudent和UndergraduateStudent里分别实现:

// UndergraduateStudent.java public class UndergraduateStudent extends Student { private String major; public UndergraduateStudent(String name, int score, String major) { super(name, score); this.major = major; } @Override public String getDescription() { return "本科生:" + name + ",专业:" + major; } }

多态的价值在这里体现得很直接:你可以写一个List<Student>,往里放本科生和研究生,遍历时调用getDescription(),运行时会自动找到对应子类的实现。参数上要注意,抽象类不能直接new,必须通过子类实例化;如果某个子类没实现抽象方法,编译器会直接报错,这是好事,能提前暴露遗漏。

提示:实验 1 到 3 的代码建议按顺序跑一遍,每跑完一个实验就手动改几个参数,比如把成绩改成负数看校验是否生效,把super调用删掉看编译报错,这种「故意改坏」的练习比只看代码有效得多。

3. 实验 4 到 5:接口、内部类与集合框架的配合

实验 4 和 5 开始进入 Java 面向对象里更「工程化」的部分——接口定义契约、内部类简化回调、集合管理批量数据。这三个东西单独学都不难,但放在一起用的时候,初学者很容易写出「接口当类用、内部类乱嵌套、集合遍历时删元素」这类问题。

3.1 实验 4:接口与内部类的典型写法

实验 4 一般要求定义一个接口,比如Sortable或Comparable的自定义版本,然后让某个类实现它。这份代码里定义了一个Scorable接口:

// Scorable.java public interface Scorable { int getScore(); default boolean isPass() { return getScore() >= 60; // 默认方法,子类可覆盖 } }

然后Student实现这个接口,并在内部用一个静态内部类做比较器:

// Student.java 片段 public class Student implements Scorable, Comparable<Student> { private String name; private int score; // ... 构造和 getter 省略 @Override public int getScore() { return score; } @Override public int compareTo(Student other) { return Integer.compare(this.score, other.score); } // 静态内部类:按姓名排序的比较器 public static class NameComparator implements Comparator<Student> { @Override public int compare(Student s1, Student s2) { return s1.name.compareTo(s2.name); } } }

这里有两个关键设计:一是default方法让接口有了默认实现,子类不必强制重写;二是静态内部类NameComparator不持有外部类实例,适合做工具型比较器。参数上,Integer.compare比手写this.score - other.score更安全,因为后者在极端值下可能溢出。如果你实验里要求匿名内部类,可以把NameComparator改成new Comparator<Student>() { ... }的形式,效果一样,只是不能复用。

3.2 实验 5:集合框架的增删改查与遍历

实验 5 通常要求用ArrayList、HashMap或HashSet管理一批学生数据,并实现按条件筛选、排序、统计。这份代码里用的是ArrayList加HashMap的组合:

import java.util.*; public class StudentManager { private List<Student> students = new ArrayList<>(); private Map<String, Student> indexByName = new HashMap<>(); public void addStudent(Student s) { students.add(s); indexByName.put(s.getName(), s); // 建立姓名索引 } // 按成绩筛选 public List<Student> filterByScore(int minScore) { List<Student> result = new ArrayList<>(); for (Student s : students) { if (s.getScore() >= minScore) { result.add(s); } } return result; } // 按成绩排序 public void sortByScore() { Collections.sort(students); // 依赖 Comparable 实现 } // 按姓名排序 public void sortByName() { students.sort(new Student.NameComparator()); } }

这段代码里最值得说的是indexByName这个HashMap——它让按姓名查找从 O(n) 降到 O(1),这是集合选型的实际意义。参数上,filterByScore的minScore是包含边界的,如果你要改成不包含,把>=改成>即可。另外,Collections.sort和List.sort的区别在于前者是静态工具方法,后者是 Java 8 引入的默认方法,两者底层都是归并排序,稳定性有保证。

注意:遍历ArrayList时如果同时删除元素,必须用Iterator.remove()或removeIf(),直接调list.remove()会抛ConcurrentModificationException,这是实验 5 里最常见的翻车点。

4. 实验 6:异常处理与文件 IO 的完整链路

实验 6 通常是综合练习,要求把前面定义的学生对象写入文件,再从文件读回来,同时处理各种异常。这份代码里用的是「文本文件 + 缓冲流」的方案,没有上序列化,因为实验环境里更看重对try-catch-finally和try-with-resources的掌握。

4.1 自定义异常与校验逻辑

先定义一个业务异常,用来区分「文件格式错误」和「IO 错误」:

// InvalidStudentFormatException.java public class InvalidStudentFormatException extends Exception { public InvalidStudentFormatException(String message) { super(message); } }

然后在解析每一行时抛出:

private Student parseLine(String line) throws InvalidStudentFormatException { String[] parts = line.split(","); if (parts.length != 2) { throw new InvalidStudentFormatException("字段数量不对:" + line); } try { String name = parts[0].trim(); int score = Integer.parseInt(parts[1].trim()); return new Student(name, score); } catch (NumberFormatException e) { throw new InvalidStudentFormatException("成绩不是数字:" + line); } }

这里的设计意图是:NumberFormatException是运行时异常,直接往外抛会让调用方难以区分「格式问题」和「文件不存在」,包一层自定义异常后,上层就能用catch (InvalidStudentFormatException e)单独处理。

4.2 文件读写与资源关闭

写文件和读文件都用try-with-resources,避免忘记关流:

// 写入 public void saveToFile(String path) throws IOException { try (BufferedWriter writer = new BufferedWriter(new FileWriter(path))) { for (Student s : students) { writer.write(s.getName() + "," + s.getScore()); writer.newLine(); } } } // 读取 public void loadFromFile(String path) throws IOException, InvalidStudentFormatException { try (BufferedReader reader = new BufferedReader(new FileReader(path))) { String line; while ((line = reader.readLine()) != null) { if (line.trim().isEmpty()) continue; // 跳过空行 students.add(parseLine(line)); } } }

参数上,FileWriter默认是覆盖模式,如果要追加,需要传第二个参数true。BufferedReader的readLine()返回null表示文件结束,这个判断不能少。另外,文件路径建议用Paths.get("data", "students.txt")这种跨平台写法,直接拼/在 Windows 上虽然也能跑,但不够规范。

提示:实验 6 跑完后,手动把数据文件里的某一行改成「张三,abc」,再运行读取程序,看自定义异常是否被正确捕获并打印出可读信息。这个验证步骤能帮你确认异常链路是通的。

5. 六个实验跑下来,这几个坑我替你踩过了

5.1 构造方法链断裂导致编译失败

现象:子类构造方法里写了super(name, score),但父类只有无参构造,编译器报「无法将类中的构造器应用到给定类型」。原因:Java 规定子类构造必须调用父类某个构造,如果父类没有无参构造,子类必须显式super(...),且必须是第一句。解决:要么给父类补一个无参构造,要么在子类第一行写super(...)。我一般建议父类保留无参构造,减少子类负担。

5.2 重写 equals 没重写 hashCode

现象:把Student对象放进HashSet后,明明两个对象属性一样,却都被保留,没去重。原因:HashSet先用hashCode定位桶,再用equals比较。只重写equals不重写hashCode,两个相等对象的哈希值不同,会被分到不同桶。解决:用 IDE 自动生成或手写hashCode,确保equals为 true 的两个对象hashCode也相等。实验 5 里如果用到HashSet,这个坑必踩。

5.3 遍历集合时删除元素抛异常

现象:for (Student s : students) { if (s.getScore() < 60) students.remove(s); }运行时报ConcurrentModificationException。原因:增强 for 循环底层用Iterator,但remove调的是集合自己的方法,导致迭代器状态和集合状态不一致。解决:改用Iterator.remove(),或者直接用students.removeIf(s -> s.getScore() < 60),后者更简洁。

5.4 文件路径写死导致换环境就找不到文件

现象:在本机跑得好好的,换台机器或换个目录就报FileNotFoundException。原因:用了绝对路径如D:/code/data.txt,或者用了相对路径但工作目录变了。解决:用Paths.get(System.getProperty("user.dir"), "data", "students.txt")获取当前工作目录下的路径,或者把文件放在src同级目录并用相对路径。实验 6 里建议把数据文件放在项目根目录的data文件夹下。

5.5 抽象类里写了构造方法却以为不能有

现象:看到抽象类里有构造方法,以为写错了,删掉后子类编译报错。原因:抽象类可以有构造方法,只是不能直接new,它的构造方法是给子类super()调用的。解决:保留抽象类的构造方法,用来初始化父类字段。实验 3 里如果父类有name和score,就应该在抽象类构造里赋值,子类通过super(name, score)传递。

6. 把实验代码改成自己的项目:三个进阶改法

这套代码如果只是跑一遍交作业,有点浪费。我一般会做三件事把它变成能写进简历的小项目。

第一,把Student换成你熟悉的业务对象,比如「订单」「商品」「传感器读数」,然后重新设计字段和校验规则。这一步练的是抽象能力——同样的继承、接口、集合结构,换一个领域能不能快速套上去。

第二,给StudentManager加一个命令行菜单,用Scanner读取用户输入,实现增删改查的交互。这个改动会逼你处理输入校验、循环控制、异常捕获,比单纯写类更有工程感。代码结构大概是这样:

public static void main(String[] args) { StudentManager manager = new StudentManager(); Scanner scanner = new Scanner(System.in); while (true) { System.out.println("1.添加 2.按成绩筛选 3.保存 4.读取 0.退出"); int choice = scanner.nextInt(); scanner.nextLine(); // 吃掉换行符 switch (choice) { case 1: System.out.print("姓名:"); String name = scanner.nextLine(); System.out.print("成绩:"); int score = scanner.nextInt(); manager.addStudent(new Student(name, score)); break; case 2: System.out.print("最低分:"); int min = scanner.nextInt(); manager.filterByScore(min).forEach(System.out::println); break; case 3: try { manager.saveToFile("data/students.txt"); } catch (IOException e) { System.out.println("保存失败:" + e.getMessage()); } break; case 0: return; } } }

第三,把文件存储从文本改成 JSON 或 CSV,用第三方库如 Jackson 或 OpenCSV 做序列化。这一步会接触到依赖管理(Maven/Gradle)和库的 API 设计,是从「实验代码」到「真实项目」的关键一跳。参数上,Jackson 的ObjectMapper默认要求类有无参构造和 getter/setter,如果字段是private且没有 getter,需要加@JsonProperty注解。

从那以后我每次拿到一套实验代码,都会先跑通、再改坏、最后换场景重写一遍,这三步走完,代码才真正变成自己的。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进