简介:这份资源是东北大学Java面向对象实验1至6的完整代码合集,面向正在修读该课程的学生以及希望系统练习OOP核心语法的自学者,可解决实验无从下手、代码结构混乱、知识点难以串联等问题。压缩包共124个文件,约51KB,以java源码与class字节码为主,辅以html说明页、dat数据文件、xml配置、gif图示及少量jar包,覆盖类与对象、封装、继承、多态、构造器、接口、异常处理、集合框架、文件流、复杂数据结构与设计模式等实验主题,从内容预览可见还包含GourmetCoffeeGUI等图形界面与目录加载相关类,能直观感受多类协作与事件监听的组织方式。目前已有2002人学习下载,说明该实验体系在课程学习者中认可度较高。读者可据此对照实验要求梳理每个实验的类结构、方法职责与调用关系,理解私有字段与公共接口的配合、父类子类扩展方式以及接口实现多态的具体写法,并借助dat与xml文件观察数据持久化与配置读取思路,从而快速完成实验并夯实面向对象编程基础。
1. 从一份 Java 实验代码说起:六个实验到底练了什么
很多人学 Java 卡在“语法都会,一写就废”的阶段。课本上的for循环、extends、interface看得懂,真让你从零写一个能跑的小系统,脑子里却一片空白。这份面向对象实验代码合集,恰好就是冲着这个痛点来的——它把 Java 面向对象的核心知识点拆成六个递进的实验,每个实验都是一个能独立编译运行的小项目,从类与对象、封装继承多态,一路做到集合框架、异常处理和文件 IO。适合正在上 Java 课、需要交实验报告的学生,也适合想系统补一遍 OOP 基础的自学者。代码结构清晰,注释到位,拿来当模板改一改就能交作业,也能当复习提纲用。下面我按“能干什么 → 怎么跑 → 怎么改”的顺序,把这份资源拆开讲。
2. 实验一到六的知识点分布:每个实验在练什么
2.1 六个实验的递进关系
这份代码不是六个孤立的练习,而是一条有依赖关系的知识链。实验一通常从最简单的类定义入手,让你写一个Student或Circle类,理解成员变量、构造方法、this关键字的用法。实验二引入封装,把字段改成private,配上getter/setter,再加重载构造方法。实验三进入继承和多态,一般会设计一个Shape抽象类,派生出Circle、Rectangle,用@Override重写area()方法。实验四往往涉及接口和抽象类的区别,比如定义一个Payable接口,让不同类去实现。实验五开始碰集合框架,用ArrayList、HashMap管理对象数据。实验六综合前面所有内容,加上异常处理和文件读写,做一个能存盘的小型管理系统。
这种安排的好处是每个实验只引入一两个新概念,不会一次性把你压垮。我见过不少同学一上来就想写完整系统,结果类之间的关系理不清,写到一半就崩了。按这个顺序走,每步都有可运行的产出,心里踏实。
2.2 每个实验的典型代码结构
以实验三的图形面积计算为例,核心代码大概长这样:
// 抽象基类,定义所有图形的公共行为 abstract class Shape { private String name; public Shape(String name) { this.name = name; } // 抽象方法,子类必须实现 public abstract double area(); public String getName() { return name; } } // 圆形子类,重写 area 方法 class Circle extends Shape { private double radius; public Circle(double radius) { super("圆形"); this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // 矩形子类 class Rectangle extends Shape { private double width; private double height; public Rectangle(double width, double height) { super("矩形"); this.width = width; this.height = height; } @Override public double area() { return width * height; } }这段代码的关键在于abstract和@Override两个点。Shape类本身不能被实例化,它只负责定义“所有图形都有面积”这个契约。子类各自实现area(),调用的时候用父类引用指向子类对象,运行时自动找到正确的方法——这就是多态。参数方面,radius、width、height都是double类型,保证计算精度。如果你把area()的返回类型改成int,编译不会报错,但结果会丢失小数部分,这是新手常踩的坑。
实验五的集合操作通常是这样组织的:
import java.util.*; public class StudentManager { // 用 ArrayList 存储学生对象,保持插入顺序 private List<Student> students = new ArrayList<>(); public void addStudent(Student s) { students.add(s); } // 按学号查找,返回 Optional 避免空指针 public Optional<Student> findById(String id) { for (Student s : students) { if (s.getId().equals(id)) { return Optional.of(s); } } return Optional.empty(); } // 按成绩降序排列 public void sortByScore() { students.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); } }这里用了List接口而不是直接写ArrayList,是为了后面换实现类方便。Optional是 Java 8 引入的,用来显式处理“可能找不到”的情况,比返回null安全。排序用了 Lambda 表达式,Double.compare处理浮点数比较比直接相减更可靠——直接相减在差值极小时可能返回 0,导致排序不稳定。
2.3 知识点与实验编号对照
| 实验编号 | 核心知识点 | 典型类/接口 | 难度 |
|---|---|---|---|
| 实验一 | 类与对象、构造方法 | Student、Circle | 入门 |
| 实验二 | 封装、方法重载 | BankAccount | 入门 |
| 实验三 | 继承、多态、抽象类 | Shape、Circle、Rectangle | 中等 |
| 实验四 | 接口、内部类 | Payable、Comparator | 中等 |
| 实验五 | 集合框架、泛型 | ArrayList、HashMap | 中等 |
| 实验六 | 异常、文件 IO、综合 | FileWriter、BufferedReader | 进阶 |
这张表可以帮你快速定位自己薄弱的部分。如果你已经会写类了,直接从实验三开始看;如果集合框架不熟,重点啃实验五。
3. 把代码跑起来:环境配置与编译执行
3.1 JDK 安装与环境变量
这份代码用的是标准 Java SE,没有依赖任何第三方库,所以只需要装一个 JDK 就行。推荐 JDK 8 或 JDK 11,这两个版本在学校机房里最常见,兼容性也最好。安装完之后验证一下:
# 检查 JDK 版本,确认安装成功 java -version javac -version如果两条命令都能输出版本号,说明环境没问题。java是运行命令,javac是编译命令,两个都要有。有些同学只装了 JRE,结果javac用不了,编译都过不去。环境变量方面,JAVA_HOME指向 JDK 安装目录,PATH里加上%JAVA_HOME%\bin(Windows)或$JAVA_HOME/bin(Mac/Linux)。配完之后最好重开一个终端窗口,不然环境变量不生效。
3.2 目录结构与编译方式
代码按实验编号分目录,每个目录下是独立的.java文件。常见结构是:
experiment1/ ├── Student.java └── Main.java experiment2/ ├── BankAccount.java └── Main.java ...每个实验目录里都有一个Main.java作为入口,里面包含public static void main(String[] args)方法。编译的时候进入对应目录:
# 进入实验三目录 cd experiment3 # 编译所有 java 文件 javac *.java # 运行主类 java Mainjavac *.java会把当前目录下所有 Java 文件一起编译,生成对应的.class文件。如果某个文件有编译错误,会直接报出来,按行号去改就行。运行的时候写java Main,不要写java Main.class,后者会报错。如果类在包里(比如package com.exp3;),编译和运行都要带上包路径,不过这份实验代码一般不带包,简化处理。
3.3 用 IDE 打开项目
如果你习惯用 IDE,IntelliJ IDEA 或 Eclipse 都可以直接打开。以 IDEA 为例,File → Open选择实验目录,IDEA 会自动识别 Java 文件并配置 SDK。如果提示没有 SDK,去Project Structure → SDKs里添加你安装的 JDK 路径。运行的时候右键Main.java,选Run 'Main.main()'就行。
用 IDE 的好处是调试方便,可以在任意一行打断点,看变量当前的值。比如实验三的多态调用,你在area()方法里打个断点,运行的时候就能看到实际调用的是哪个子类的方法。这个对理解多态特别有帮助,比光看代码强得多。
提示:如果 IDEA 打开后代码全是红色报错,先检查
File → Project Structure → Project里的 Language Level 是不是设成了 8 以上。有些老版本默认是 5,不支持 Lambda 和Optional。
4. 核心模块拆解:从类设计到数据流转
4.1 实验六的综合管理系统架构
实验六通常是前面所有知识点的汇总,做一个带文件存储的管理系统。它的类结构一般分三层:实体类、管理类、入口类。实体类负责存数据,管理类负责增删改查,入口类负责和用户交互。
// 实体类:只负责存数据,不掺业务逻辑 public class Student { private String id; private String name; private double score; public Student(String id, String name, double score) { this.id = id; this.name = name; this.score = score; } // getter/setter 省略,实际代码里都有 public String toCsv() { return id + "," + name + "," + score; } public static Student fromCsv(String line) { String[] parts = line.split(","); return new Student(parts[0], parts[1], Double.parseDouble(parts[2])); } }toCsv()和fromCsv()是一对,负责对象和字符串之间的转换。存文件的时候调toCsv(),读文件的时候调fromCsv()。这种设计叫序列化/反序列化,是文件存储的基础。注意fromCsv()里用了Double.parseDouble,如果文件里的成绩不是合法数字,这里会抛NumberFormatException,需要在调用处捕获。
管理类负责所有业务操作:
import java.io.*; import java.util.*; public class StudentManager { private List<Student> students = new ArrayList<>(); private static final String FILE_PATH = "students.csv"; // 从文件加载数据,文件不存在时返回空列表 public void loadFromFile() { File file = new File(FILE_PATH); if (!file.exists()) { return; } try (BufferedReader reader = new BufferedReader(new FileReader(file))) { String line; while ((line = reader.readLine()) != null) { if (!line.trim().isEmpty()) { students.add(Student.fromCsv(line)); } } } catch (IOException e) { System.err.println("读取文件失败:" + e.getMessage()); } } // 保存数据到文件 public void saveToFile() { try (BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_PATH))) { for (Student s : students) { writer.write(s.toCsv()); writer.newLine(); } } catch (IOException e) { System.err.println("写入文件失败:" + e.getMessage()); } } public void addStudent(Student s) { students.add(s); } public List<Student> getAll() { return Collections.unmodifiableList(students); } }这里有几个值得说的点。第一,try-with-resources语法自动关闭流,不用手动写finally块,代码更干净。第二,Collections.unmodifiableList返回一个只读视图,防止外部代码直接修改内部列表,这是封装思想的体现。第三,文件路径用的是相对路径students.csv,文件会生成在程序运行目录下。如果你在 IDE 里运行,文件可能出现在项目根目录,而不是src里,找不到文件的时候先确认运行目录。
4.2 异常处理的分层策略
实验六会涉及异常处理,常见的做法是分两层:底层方法抛出异常,上层方法捕获并处理。比如fromCsv解析失败时抛IllegalArgumentException,loadFromFile捕获后决定是跳过这一行还是终止加载。
public static Student fromCsv(String line) { String[] parts = line.split(","); if (parts.length != 3) { throw new IllegalArgumentException("CSV 格式错误:" + line); } try { double score = Double.parseDouble(parts[2]); return new Student(parts[0], parts[1], score); } catch (NumberFormatException e) { throw new IllegalArgumentException("成绩不是合法数字:" + parts[2], e); } }这样做的好处是异常信息更明确,调用方知道具体哪里出了问题。IllegalArgumentException表示参数不合法,NumberFormatException是它的子类,这里包了一层并保留了原始异常(第二个参数e),方便排查。很多同学写异常处理就是e.printStackTrace()完事,其实把异常信息写清楚,后面调试能省很多时间。
4.3 集合选型的实际考量
实验五和实验六会用到集合,选ArrayList还是LinkedList,选HashMap还是TreeMap,要看具体场景。这份代码里学生列表用ArrayList,因为主要是遍历和追加,随机访问多,ArrayList的数组结构更合适。如果要频繁在中间插入删除,LinkedList更好,但实验场景一般用不上。
HashMap用来做学号到学生的映射,查找时间复杂度 O(1)。如果需要对学号排序输出,可以换成TreeMap,它会自动按 key 排序,但插入和查找变成 O(log n)。我一般会先问自己:这个数据是用来查的,还是用来排的?查得多就用HashMap,排得多就用TreeMap。
注意:
HashMap的 key 如果是自定义对象,必须正确重写hashCode()和equals(),否则查找会失效。实验里一般用String或Integer做 key,不会遇到这个问题,但二次开发时要留意。
5. 避坑与排查:编译运行中的常见问题
5.1 中文乱码
现象:程序输出到控制台的中文变成问号或方块。原因:Windows 默认编码是 GBK,而 Java 源文件可能是 UTF-8,编译时没指定编码。解决:编译时加-encoding UTF-8,即javac -encoding UTF-8 *.java。如果用的是 IDEA,去Settings → Editor → File Encodings把全局编码和项目编码都设成 UTF-8。
5.2 找不到主类
现象:运行java Main时报Could not find or load main class Main。原因:要么没编译,要么类名和文件名不一致,要么带了包名但没建对应目录。解决:先确认Main.class文件存在,再检查Main.java里public class Main的类名是否和文件名完全一致(大小写敏感)。如果类在包里,运行时要写全限定名,比如java com.exp1.Main。
5.3 文件读写路径错误
现象:实验六运行时提示FileNotFoundException或文件生成了但找不到。原因:相对路径是相对于“当前工作目录”,不是相对于.java文件所在目录。在 IDE 里运行,工作目录通常是项目根目录;在命令行运行,工作目录是你执行java命令时所在的目录。解决:用绝对路径测试,或者打印System.getProperty("user.dir")看当前工作目录到底在哪。
5.4 空指针异常
现象:运行时抛NullPointerException,但不知道哪一行。原因:调用了null对象的方法或访问了null对象的字段。解决:在可能为null的地方加判断,或者用Optional包装返回值。调试时看异常堆栈的第一行,会指出具体是哪个类的哪一行。实验五的findById如果返回null而不是Optional,调用方忘了判空就会中招。
5.5 集合遍历时删除元素
现象:用for-each遍历ArrayList时删除元素,抛ConcurrentModificationException。原因:for-each底层用的是迭代器,遍历过程中修改集合结构会让迭代器失效。解决:用Iterator.remove()方法删除,或者用removeIf()。比如students.removeIf(s -> s.getScore() < 60)一行就能删掉所有不及格的,比手动遍历安全。
6. 二次开发与改造思路:从实验代码到课程设计
6.1 把控制台程序改成图形界面
实验代码都是控制台交互,如果你想拿它做课程设计,加一个 Swing 或 JavaFX 界面会加分不少。改造思路是保留StudentManager不动,新建一个MainFrame类,把按钮事件绑定到管理类的方法上。比如“添加学生”按钮的监听器里,从文本框读数据,构造Student对象,调manager.addStudent(),然后刷新表格。
// Swing 按钮事件绑定示例 addButton.addActionListener(e -> { String id = idField.getText().trim(); String name = nameField.getText().trim(); double score = Double.parseDouble(scoreField.getText().trim()); manager.addStudent(new Student(id, name, score)); refreshTable(); // 刷新 JTable 显示 });这样改的好处是业务逻辑和界面分离,管理类不用动,测试也方便。我一般会先保证控制台版本跑通,再加界面,不然出了问题分不清是逻辑错还是界面错。
6.2 换存储方式:从 CSV 到数据库
CSV 文件适合小数据量,但并发写入和查询效率都不行。二次开发时可以换成 SQLite,不用装数据库服务器,一个文件就是一个库。用 JDBC 连接,把loadFromFile和saveToFile换成 SQL 语句。
// 用 JDBC 替换文件存储 public void saveToDb(Student s) { String sql = "INSERT INTO student(id, name, score) VALUES(?, ?, ?)"; try (Connection conn = DriverManager.getConnection("jdbc:sqlite:student.db"); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getId()); ps.setString(2, s.getName()); ps.setDouble(3, s.getScore()); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }用PreparedStatement而不是Statement拼接字符串,能防止 SQL 注入,也更高效。改造的时候注意异常类型从IOException变成SQLException,捕获逻辑要跟着改。
6.3 加单元测试保证改造不出错
改代码最怕改出 bug。加 JUnit 测试是个好习惯,每次改完跑一遍,心里有底。比如给StudentManager写测试:
import org.junit.Test; import static org.junit.Assert.*; public class StudentManagerTest { @Test public void testAddAndFind() { StudentManager manager = new StudentManager(); manager.addStudent(new Student("001", "张三", 85.5)); assertEquals(1, manager.getAll().size()); assertEquals("张三", manager.getAll().get(0).getName()); } }测试覆盖增删改查和边界情况(空列表、重复学号、非法成绩),改代码的时候就有安全网。我一般会先写测试再改代码,这样能确保改造不破坏原有功能。
6.4 一个容易忽略的细节:equals 和 hashCode
如果你在实验五里用Student对象做HashMap的 key,或者用List.contains()判断学生是否存在,必须重写equals()和hashCode()。默认的实现比较的是对象地址,两个内容相同但地址不同的对象会被认为不相等。
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return Objects.equals(id, student.id); } @Override public int hashCode() { return Objects.hash(id); }这里用学号作为判断依据,因为学号是唯一的。Objects.equals和Objects.hash是 Java 7 引入的工具方法,能自动处理null,比手写if (id != null && id.equals(...))简洁。重写之后,list.contains(new Student("001", ...))就能正确返回true了。
从那以后我每次改完代码,都会先跑一遍原有的测试用例,确认没破坏已有功能,再提交。这个习惯帮我省了不少返工的时间。希望这份实验代码和上面的改造思路,能帮你把 Java 面向对象真正用起来,而不只是应付考试。
本文还有配套的精品资源,点击获取