ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java面向对象实验代码全解析:从类与对象到文件I/O的六个实战

Java面向对象实验代码全解析:从类与对象到文件I/O的六个实战

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

1. 从一份 Java 实验代码说起:六个实验到底练了什么

很多同学学完 Java 语法,写个Hello World没问题,一到“面向对象”就卡壳——类怎么设计、继承怎么用、多态到底解决什么问题,书本上的例子看懂了,自己动手还是懵。这份资源就是一套完整的 Java 面向对象实验代码,从实验一到实验六,覆盖了类与对象、继承与多态、接口与抽象类、集合框架、异常处理、文件 I/O 这几个核心模块。每个实验都是独立可运行的工程,不是零散代码片段,适合正在上 Java 课、需要交实验报告,或者想通过动手把 OOP 思想吃透的开发者。如果你已经能写基本语法但一写多类协作就乱,这套代码值得拆一遍。

2. 实验一到六的模块拆解:每个实验在练什么

2.1 实验一:类与对象的基础建模

实验一通常从最简单的类定义开始,比如定义一个Student类,包含姓名、学号、成绩等属性,再写构造方法、getter/setter,最后在main里创建对象并调用方法。这个实验看起来简单,但它是后面所有实验的地基。我见过不少人直接跳过这个实验去写继承,结果连this和构造方法的重载都搞不清楚。

核心代码结构一般是这样:

// Student.java public class Student { private String name; private String id; private double score; // 无参构造 public Student() {} // 带参构造,用于初始化对象 public Student(String name, String id, double score) { this.name = name; this.id = id; this.score = score; } // getter/setter 省略,注意封装性 public void printInfo() { System.out.println("姓名:" + name + ",学号:" + id + ",成绩:" + score); } }

这里的关键是理解private修饰符的作用——属性私有,对外提供公共方法访问。很多初学者会直接把属性写成public,编译也能过,但这就失去了封装的意义。实验一的评分点往往就在“是否使用了封装”。

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

实验二引入继承关系,典型场景是Person作为父类,Student和Teacher作为子类。子类通过extends继承父类的属性和方法,同时可以重写父类的方法来实现自己的行为。这个实验的核心是理解super关键字和@Override注解。

// Person.java public class Person { protected String name; protected int age; public Person(String name, int age) { this.name = name; this.age = age; } public void introduce() { System.out.println("我是" + name + ",今年" + age + "岁"); } } // Student.java public class Student extends Person { private String major; public Student(String name, int age, String major) { super(name, age); // 调用父类构造方法 this.major = major; } @Override public void introduce() { super.introduce(); // 复用父类逻辑 System.out.println("我的专业是" + major); } }

super(name, age)必须放在子类构造方法的第一行,这是 Java 的硬性规定。@Override注解不是必须的,但加上之后编译器会帮你检查方法签名是否匹配,避免拼写错误导致重写失败。实验二常见的坑是把父类属性写成private,子类访问不到,这时候要么改成protected,要么通过 getter/setter 访问。

2.3 实验三:多态与抽象类

实验三开始涉及多态——父类引用指向子类对象,运行时根据实际类型调用对应的方法。典型例子是定义一个抽象类Shape,包含抽象方法area(),然后Circle、Rectangle分别实现。这个实验的目的是让你理解“面向接口编程”而不是“面向实现编程”。

// Shape.java public abstract class Shape { public abstract double area(); // 抽象方法,子类必须实现 } // Circle.java public class Circle extends Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public double area() { return Math.PI * radius * radius; } } // 测试类 public class TestShape { public static void main(String[] args) { Shape s1 = new Circle(5.0); Shape s2 = new Rectangle(3.0, 4.0); System.out.println(s1.area()); // 运行时决定调用哪个 area() System.out.println(s2.area()); } }

多态的好处在于,你可以写一个方法接收Shape类型的参数,不管传入的是圆还是矩形,都能正确计算面积。如果后面新增一个Triangle类,不需要修改任何已有代码,直接继承Shape就行。这就是开闭原则的体现。

2.4 实验四:接口与集合框架

实验四一般会把接口和集合结合起来用。比如定义一个Comparable接口的实现,然后用ArrayList存储对象,再调用Collections.sort()排序。这个实验的难点在于理解接口和抽象类的区别——接口定义“能做什么”,抽象类定义“是什么”。

// 定义接口 public interface Printable { void print(); } // 实现类 public class Book implements Printable, Comparable<Book> { private String title; private double price; public Book(String title, double price) { this.title = title; this.price = price; } @Override public void print() { System.out.println(title + ":" + price + "元"); } @Override public int compareTo(Book other) { return Double.compare(this.price, other.price); } } // 使用集合 List<Book> books = new ArrayList<>(); books.add(new Book("Java编程", 59.9)); books.add(new Book("数据结构", 45.0)); Collections.sort(books); // 按价格排序 for (Book b : books) { b.print(); }

Comparable接口的compareTo方法返回值决定了排序顺序:负数表示当前对象小,正数表示大,零表示相等。这里用Double.compare是标准做法,避免直接相减导致的精度问题。集合框架这块,ArrayList和LinkedList的区别、HashMap的键值对操作,实验四通常都会涉及。

2.5 实验五:异常处理

实验五专门练异常。一般会要求自定义一个异常类,然后在方法中throw,调用方用try-catch捕获。这个实验的核心是理解 checked exception 和 unchecked exception 的区别,以及finally块的作用。

// 自定义异常 public class InvalidScoreException extends Exception { public InvalidScoreException(String message) { super(message); } } // 业务方法 public void setScore(double score) throws InvalidScoreException { if (score < 0 || score > 100) { throw new InvalidScoreException("成绩必须在0到100之间:" + score); } this.score = score; } // 调用方 try { student.setScore(150); } catch (InvalidScoreException e) { System.out.println("输入有误:" + e.getMessage()); } finally { System.out.println("成绩设置操作结束"); }

继承Exception的是 checked exception,调用方必须处理;继承RuntimeException的是 unchecked,编译器不强制捕获。实验五的常见错误是自定义异常继承了RuntimeException,然后调用方不写try-catch,运行时直接崩溃。另外finally块里的代码无论是否发生异常都会执行,常用来释放资源。

2.6 实验六:文件 I/O 与综合应用

实验六通常是综合前面的知识点,做一个带文件读写的小系统。比如学生信息管理系统,支持从文件读取学生数据、增删改查、再写回文件。这个实验会用到File、FileReader、BufferedReader、FileWriter等类。

// 读取文件 public List<Student> loadFromFile(String path) throws IOException { List<Student> list = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new FileReader(path))) { String line; while ((line = br.readLine()) != null) { String[] parts = line.split(","); list.add(new Student(parts[0], parts[1], Double.parseDouble(parts[2]))); } } return list; } // 写入文件 public void saveToFile(String path, List<Student> list) throws IOException { try (BufferedWriter bw = new BufferedWriter(new FileWriter(path))) { for (Student s : list) { bw.write(s.getName() + "," + s.getId() + "," + s.getScore()); bw.newLine(); } } }

try-with-resources是 Java 7 引入的语法,括号里声明的资源会在 try 块结束后自动关闭,不用手动写finally里的close()。文件路径建议用相对路径,比如"data/students.txt",这样换台机器也能跑。实验六的评分点通常包括:文件不存在时是否创建、读写是否用了缓冲流、异常是否妥善处理。

3. 怎么把这份代码跑起来:环境配置与运行步骤

3.1 JDK 安装与环境变量配置

这份代码基于标准 Java SE,没有用到第三方框架,所以只需要装 JDK 就行。推荐 JDK 8 或 JDK 11,这两个版本在学校环境里最稳。安装完之后配置JAVA_HOME和PATH:

# Windows 下在系统环境变量里添加 JAVA_HOME = C:\Program Files\Java\jdk1.8.0_301 PATH = %JAVA_HOME%\bin;%PATH% # macOS/Linux 下编辑 ~/.bash_profile 或 ~/.zshrc export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_301.jdk/Contents/Home export PATH=$JAVA_HOME/bin:$PATH

配置完之后在终端执行java -version和javac -version,能正确输出版本号就说明环境没问题。如果提示“不是内部或外部命令”,说明PATH没配好,检查一下路径里有没有空格或拼写错误。

3.2 用命令行编译和运行

每个实验的代码通常放在一个以实验编号命名的文件夹里,比如experiment1、experiment2。进入对应目录后,先编译再运行:

# 编译所有 .java 文件 javac -d out src/*.java # 运行主类 java -cp out com.experiment.Main

-d out表示把编译后的.class文件放到out目录,保持包结构。-cp指定 classpath,告诉 JVM 去哪里找类文件。如果代码里用了包声明(比如package com.experiment;),编译后的目录结构会自动对应。

3.3 用 IDE 导入项目

如果习惯用 IDE,IntelliJ IDEA 或 Eclipse 都可以直接导入。以 IDEA 为例:File -> New -> Project from Existing Sources,选择实验代码的根目录,IDEA 会自动识别src目录和包结构。如果识别不出来,手动把src标记为 Sources Root。运行的时候右键主类,选择Run 'Main.main()'就行。

提示:如果导入后报“找不到符号”或“包不存在”,先检查src目录是否被正确标记,再检查 JDK 版本是否和代码兼容。实验代码一般不会用到高版本特性,JDK 8 足够。

4. 避坑与常见问题:跑代码时容易翻车的几个点

4.1 中文乱码

现象:控制台输出的中文变成问号或方块。原因:Windows 默认编码是 GBK,而代码文件可能是 UTF-8,编译时没指定编码。解决:编译时加-encoding UTF-8,运行时加-Dfile.encoding=UTF-8:

javac -encoding UTF-8 -d out src/*.java java -Dfile.encoding=UTF-8 -cp out com.experiment.Main

4.2 空指针异常

现象:运行时报NullPointerException,定位到某一行。原因:对象没初始化就调用了方法或访问了属性。比如Student s; s.getName();,s还是 null。解决:创建对象时确保调用了构造方法,Student s = new Student();。集合操作时先判断是否为 null,或者用Optional包装。

4.3 文件路径错误

现象:实验六读写文件时报FileNotFoundException。原因:用了绝对路径,换台机器路径不存在;或者相对路径的基准目录不是项目根目录。解决:统一用相对路径,并且在代码里打印当前工作目录确认:

System.out.println(System.getProperty("user.dir"));

然后把文件放在这个目录下,或者用getClass().getResourceAsStream()从 classpath 读取。

4.4 集合遍历时删除元素

现象:用for-each遍历ArrayList时删除元素,抛ConcurrentModificationException。原因:for-each底层用的是Iterator,遍历过程中修改集合结构会导致迭代器失效。解决:用Iterator显式遍历,调用iterator.remove():

Iterator<Student> it = list.iterator(); while (it.hasNext()) { Student s = it.next(); if (s.getScore() < 60) { it.remove(); // 安全删除 } }

4.5 重写 equals 和 hashCode

现象:把对象放进HashSet或作为HashMap的键,去重或查找失败。原因:只重写了equals没重写hashCode,或者两个都没重写,导致两个内容相同的对象被认为是不同的键。解决:同时重写equals和hashCode,IDEA 可以自动生成:

@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); }

5. 二次开发与改造:从实验代码到能用的项目

5.1 把控制台输出改成图形界面

实验代码基本都是控制台交互,如果想改成 Swing 或 JavaFX 界面,核心思路是把main里的Scanner输入换成按钮事件,把System.out.println换成JTextArea.append。比如实验六的学生管理系统,可以加一个JFrame,上面放表格JTable显示学生列表,下面放输入框和按钮。数据层不用动,只改 UI 层。

// 简单的 Swing 改造示例 JFrame frame = new JFrame("学生管理系统"); JTable table = new JTable(new StudentTableModel(studentList)); JButton addBtn = new JButton("添加"); addBtn.addActionListener(e -> { // 弹出输入对话框,添加学生后刷新表格 String name = JOptionPane.showInputDialog("姓名:"); // ... 省略其他字段 studentList.add(new Student(name, id, score)); table.updateUI(); }); frame.add(new JScrollPane(table), BorderLayout.CENTER); frame.add(addBtn, BorderLayout.SOUTH); frame.setSize(600, 400); frame.setVisible(true);

StudentTableModel需要继承AbstractTableModel,实现getRowCount、getColumnCount、getValueAt三个方法。这样表格就能自动绑定数据源,增删改后调用fireTableDataChanged()刷新。

5.2 把数据存储从文件换成数据库

实验六用的是文本文件,实际项目里更常用数据库。改造步骤:加 JDBC 驱动依赖,把loadFromFile和saveToFile换成select和insert语句。以 MySQL 为例:

// 加载驱动(JDBC 4.0 之后可以省略) Class.forName("com.mysql.cj.jdbc.Driver"); // 获取连接 Connection conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/school?useSSL=false&serverTimezone=UTC", "root", "password" ); // 查询 PreparedStatement ps = conn.prepareStatement("SELECT * FROM student"); ResultSet rs = ps.executeQuery(); while (rs.next()) { list.add(new Student(rs.getString("name"), rs.getString("id"), rs.getDouble("score"))); } // 插入 PreparedStatement insert = conn.prepareStatement( "INSERT INTO student(name, id, score) VALUES(?, ?, ?)" ); insert.setString(1, s.getName()); insert.setString(2, s.getId()); insert.setDouble(3, s.getScore()); insert.executeUpdate();

用PreparedStatement而不是Statement,可以防止 SQL 注入,而且参数设置更清晰。连接用完记得关闭,或者用连接池(比如 HikariCP)管理。

5.3 加日志和单元测试

实验代码里通常用System.out.println打印调试信息,实际项目里应该用日志框架。把println换成Logger:

private static final Logger logger = Logger.getLogger(Student.class.getName()); // 替代 System.out.println logger.info("学生信息已加载,共 " + list.size() + " 条"); logger.warning("成绩超出范围:" + score);

单元测试用 JUnit,给每个实验的核心方法写测试用例:

@Test public void testSetScoreValid() throws InvalidScoreException { Student s = new Student(); s.setScore(85); assertEquals(85, s.getScore(), 0.01); } @Test(expected = InvalidScoreException.class) public void testSetScoreInvalid() throws InvalidScoreException { Student s = new Student(); s.setScore(-10); // 应该抛异常 }

5.4 一个我踩过的坑

刚开始改实验六的时候,我直接把文件读写换成了数据库,结果忘了处理SQLException。因为SQLException是 checked exception,编译器强制要求捕获,我图省事直接在方法签名上throws SQLException,一路往上抛,最后在main里 catch 了一个寂寞——什么都没做,异常被吞了,程序出错也不报。后来我养成一个习惯:每个catch块里至少打一条日志,或者用throw new RuntimeException(e)包装后继续往上抛,绝不写空 catch。从那以后我每次改数据层代码,都强制走一遍“异常路径测试”——故意传错参数、断开数据库连接,看程序是不是能给出有意义的错误提示,而不是静默失败。希望这个习惯也能帮到你。

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

RELATED READING

延伸阅读

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