ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java面向对象实验1到6全解析:类与对象、封装、继承、多态、集合与异常处理

Java面向对象实验1到6全解析:类与对象、封装、继承、多态、集合与异常处理

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

1. 从零到一:Java 面向对象实验的知识地图长什么样

很多人复习 Java 面向对象实验时,第一反应是翻代码、背语法,结果到了考场或项目里还是不知道类该怎么拆、接口该不该抽。这套实验代码 1 到 6 的价值不在于“能跑”,而在于它把面向对象的核心概念按认知顺序串成了一条线:从类与对象的基本建模,到封装、继承、多态三大特性,再到接口与抽象类的取舍,最后落到集合与异常处理的综合运用。梳理下来会发现,六个实验其实对应六个递进的能力台阶,每个台阶都在解决上一阶段遗留的设计问题。如果你正在系统学 Java 或者准备实验考核,这份代码就是一张可以逐题对照的知识地图,帮你把零散语法点挂到面向对象的骨架上。

2. 实验 1 到 2:类与对象、封装——从“能跑”到“像样”

2.1 类与对象:第一个实验到底在练什么

实验 1 通常要求定义一个简单的类,比如学生类、矩形类或银行账户类,然后创建对象、调用方法、访问属性。很多人觉得这太简单,随便写个main方法就交了。但梳理下来会发现,这个实验真正训练的是“识别职责”——哪些数据应该放在类里,哪些行为应该由对象自己完成。

以银行账户为例,一个合格的实验 1 代码应该长这样:

public class BankAccount { private String ownerName; private double balance; public BankAccount(String ownerName, double initialBalance) { this.ownerName = ownerName; this.balance = initialBalance; } public void deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("存款金额必须为正数"); } balance += amount; } public void withdraw(double amount) { if (amount > balance) { throw new IllegalStateException("余额不足"); } balance -= amount; } public double getBalance() { return balance; } }

这段代码的关键不在语法,而在三个设计决策:属性用private修饰、构造函数负责初始化、存取款方法自带校验逻辑。实验 1 最容易翻车的地方就是把所有属性写成public,然后在main方法里直接改值。这样写虽然能跑,但完全丢掉了面向对象的第一层意义——对象应该自己管理自己的状态。

参数说明:ownerName和balance是账户的核心状态,deposit和withdraw是行为,getBalance是只读访问接口。构造函数里的initialBalance允许调用方在创建对象时指定初始金额,避免创建出“无主账户”。

2.2 封装:为什么第二个实验总在改第一个实验的代码

实验 2 通常要求对实验 1 的类进行改造,加入private属性、getter/setter方法,并可能引入包的概念。这个实验的典型场景是:把学生类的年龄属性改成私有,然后通过setAge方法限制年龄范围。

public class Student { private String name; private int age; public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("年龄不合法"); } this.age = age; } public int getAge() { return age; } }

这里最容易忽略的是:setter方法不是简单赋值,而是数据校验的入口。很多同学写setAge时直接this.age = age;,那封装就白做了。封装的本质是“控制访问”,不是“加个 get/set 就完事”。

重点难点在于:什么时候该提供setter,什么时候只提供getter。比如账户余额,通常只提供getBalance,不提供setBalance,因为余额只能通过存取款行为改变。这个判断标准在实验 2 里就要建立起来,否则后面实验 3 的继承会写得很乱。

提示:实验 1 和实验 2 的代码要对照着看,重点观察“哪些属性从 public 变成了 private”“哪些方法从直接赋值变成了带校验的 setter”。这个变化过程就是封装思想的落地。

3. 实验 3 到 4:继承、多态与抽象类——代码复用的分水岭

3.1 继承:extends 背后的“是一个”关系

实验 3 通常要求设计一个继承体系,比如Person作为父类,Student和Teacher作为子类。这个实验的核心不是记住extends关键字,而是理解“子类是一种父类”的语义。

public class Person { protected String name; protected int id; public Person(String name, int id) { this.name = name; this.id = id; } public String getInfo() { return "姓名:" + name + ",编号:" + id; } } public class Student extends Person { private String major; public Student(String name, int id, String major) { super(name, id); this.major = major; } @Override public String getInfo() { return super.getInfo() + ",专业:" + major; } }

这段代码里,super(name, id)调用父类构造函数,@Override注解表示重写父类方法。实验 3 最容易踩的坑是:子类构造函数没有显式调用super(...),导致父类属性没有被正确初始化。另一个坑是父类属性用了private,子类无法直接访问,只能用protected或通过getter访问。

重点难点在于:继承的层次不宜过深,一般不超过三层。实验 3 里如果出现A extends B extends C extends D,就要反思是不是设计过度了。梳理下来会发现,继承的真正价值是“把公共代码上提到父类”,而不是“为了继承而继承”。

3.2 多态:父类引用指向子类对象的实战意义

实验 4 通常要求用多态实现一个简单的“图形面积计算”或“员工薪资计算”场景。核心写法是:父类引用数组存放不同子类对象,循环调用同一个方法,运行时根据实际类型执行不同逻辑。

abstract class Shape { abstract double area(); } class Circle extends Shape { private double radius; Circle(double radius) { this.radius = radius; } @Override double area() { return Math.PI * radius * radius; } } class Rectangle extends Shape { private double width, height; Rectangle(double width, double height) { this.width = width; this.height = height; } @Override double area() { return width * height; } } public class Main { public static void main(String[] args) { Shape[] shapes = { new Circle(3), new Rectangle(4, 5) }; for (Shape s : shapes) { System.out.println("面积:" + s.area()); } } }

这段代码的关键在于Shape[] shapes这个数组——它声明的是父类类型,但实际存放的是子类对象。循环里的s.area()在编译时看的是Shape类有没有area方法,运行时才决定调用Circle还是Rectangle的实现。这就是多态的核心:编译看左边,运行看右边。

实验 4 最容易出错的地方是:父类方法没有用abstract修饰,或者子类重写时方法签名不一致(比如参数类型或返回值不同),导致多态失效。另一个常见问题是:在main方法里用instanceof加强制类型转换来分别处理,那就退化成面向过程了,完全没用到多态。

注意:抽象类不能实例化,但可以声明引用变量。Shape s = new Shape();会直接编译报错,这个错误在实验 4 里出现频率很高。

3.3 抽象类与接口:实验 4 后半段的取舍逻辑

实验 4 往往还会引入接口,比如Comparable或自定义的Printable接口。这时候就要区分抽象类和接口的使用场景:抽象类用于“同类事物的公共模板”,接口用于“跨类事物的能力约定”。

interface Printable { void print(); } class Report implements Printable { private String content; Report(String content) { this.content = content; } @Override public void print() { System.out.println("打印报告:" + content); } }

如果实验 4 要求同时使用抽象类和接口,典型的做法是:抽象类定义“是什么”,接口定义“能做什么”。比如abstract class Employee定义员工的基本属性,interface Payable定义“可计算薪资”的能力。这样设计的好处是,一个类只能继承一个抽象类,但可以实现多个接口,灵活性更高。

重点难点:接口中的方法默认是public abstract的,不能有方法体(Java 8 之后可以有default方法,但实验代码通常不涉及)。实现接口时,方法必须用public修饰,否则编译报错。这个细节在实验 4 里经常被忽略。

4. 实验 5 到 6:集合、异常与综合实战——从单点到系统

4.1 集合框架:ArrayList 与 HashMap 的实验用法

实验 5 通常要求用集合存储对象,比如用ArrayList管理学生列表,用HashMap存储学号与学生的映射关系。这个实验的核心是:根据场景选择正确的集合类型。

集合类型底层结构适用场景实验中的典型用法
ArrayList动态数组频繁按索引访问、尾部插入存储学生列表,遍历输出
LinkedList双向链表频繁头部插入删除实验代码中较少出现
HashMap哈希表键值对快速查找学号到学生对象的映射
HashSet哈希表去重存储不重复的标签或类别
import java.util.*; public class StudentManager { private Map<String, Student> studentMap = new HashMap<>(); public void addStudent(Student s) { studentMap.put(s.getId(), s); } public Student findById(String id) { return studentMap.get(id); } public List<Student> getAllStudents() { return new ArrayList<>(studentMap.values()); } }

这段代码里,HashMap负责按学号快速查找,ArrayList负责返回有序列表。实验 5 最容易犯的错是:用ArrayList存学生后,每次查找都遍历整个列表,时间复杂度 O(n)。如果实验要求“按学号查找”,就应该用HashMap,查找时间接近 O(1)。

重点难点:HashMap的key必须正确重写hashCode和equals方法,否则查找会失败。实验代码里如果自定义对象作为key,一定要检查这两个方法有没有重写。

4.2 异常处理:try-catch-finally 的实验写法

实验 6 通常要求加入异常处理,比如自定义异常类、在方法中抛出异常、在调用处捕获并处理。这个实验的核心是:区分受检异常和非受检异常,并合理使用try-catch-finally。

class InsufficientBalanceException extends Exception { public InsufficientBalanceException(String message) { super(message); } } public class Account { private double balance; public void withdraw(double amount) throws InsufficientBalanceException { if (amount > balance) { throw new InsufficientBalanceException("余额不足,当前余额:" + balance); } balance -= amount; } }

调用方代码:

public class Main { public static void main(String[] args) { Account acc = new Account(); try { acc.withdraw(1000); } catch (InsufficientBalanceException e) { System.out.println("取款失败:" + e.getMessage()); } finally { System.out.println("交易结束"); } } }

这里的关键点有三个:自定义异常继承Exception表示受检异常,调用方必须处理;throws声明在方法签名上,throw写在方法体内;finally块无论是否发生异常都会执行,适合放资源释放代码。

实验 6 最容易漏掉的是:finally块里如果也有return语句,会覆盖try块里的返回值。这个坑在实验代码里不一定出现,但复习时要心里有数。

4.3 综合实战:六个实验串起来看设计模式雏形

把实验 1 到 6 连起来看,会发现一条清晰的设计演进线:实验 1 定义类,实验 2 封装属性,实验 3 抽取父类,实验 4 用多态统一接口,实验 5 用集合管理对象,实验 6 用异常处理边界情况。这套流程本身就是一个小型系统的开发缩影。

梳理下来会发现,实验 5 和实验 6 的代码里已经隐约出现了“分层”的思想:StudentManager负责管理集合,Student负责自身数据,Main负责调用。如果实验 6 要求写一个完整的控制台程序,典型的代码结构应该是:

public class Main { public static void main(String[] args) { StudentManager manager = new StudentManager(); Scanner scanner = new Scanner(System.in); while (true) { System.out.println("1. 添加学生 2. 查找学生 3. 退出"); int choice = scanner.nextInt(); scanner.nextLine(); try { switch (choice) { case 1: System.out.print("输入学号:"); String id = scanner.nextLine(); System.out.print("输入姓名:"); String name = scanner.nextLine(); manager.addStudent(new Student(id, name)); break; case 2: System.out.print("输入学号:"); Student s = manager.findById(scanner.nextLine()); if (s != null) { System.out.println(s.getInfo()); } else { System.out.println("未找到该学生"); } break; case 3: return; default: System.out.println("无效选项"); } } catch (Exception e) { System.out.println("操作异常:" + e.getMessage()); } } } }

这段代码把集合、异常、面向对象都串起来了。重点在于:try-catch包住了整个switch,任何一步出错都不会导致程序崩溃。实验 6 的评分点往往就在这里——程序能不能在用户输入非法数据时继续运行。

5. 易漏点与易错点:六个实验里最容易翻车的五个地方

5.1 构造函数没有初始化所有属性

易漏点:子类构造函数忘记调用super(...),或者父类没有提供带参构造函数。为什么漏:实验 3 里父类如果只写了带参构造函数,子类不显式调用super就会编译报错。很多人直接加个无参构造函数“糊弄”过去,结果父类属性全是默认值。怎么补:父类只提供带参构造函数时,子类必须用super(参数)显式调用。如果父类有无参构造函数,子类可以省略super(),但属性初始化要自己保证。

5.2 重写 equals 却没重写 hashCode

易漏点:自定义对象作为HashMap的 key 时,只重写了equals,没重写hashCode。为什么漏:实验 5 里用学号作为 key 时,如果 key 是String类型,Java 已经帮你重写好了。但一旦用自定义对象作为 key,两个equals相等的对象hashCode不同,HashMap就找不到。怎么补:用 IDE 自动生成equals和hashCode,或者手动确保两个方法基于同一组属性计算。

5.3 多态调用时方法签名不一致

易漏点:子类“重写”父类方法时,参数类型或返回值写错了,变成了重载而不是重写。为什么漏:实验 4 里父类方法double area(),子类写成double area(int x),编译能过,但多态调用时永远执行父类方法。怎么补:重写时加@Override注解,编译器会帮你检查签名是否一致。

5.4 finally 块里写 return 覆盖返回值

易漏点:try块里return a;,finally块里return b;,最终返回b。为什么漏:实验 6 里如果finally块用来关闭资源,顺手写了return,就会吞掉try块的返回值。怎么补:finally块只做资源释放,不要写return、break、continue。

5.5 集合遍历时删除元素导致异常

易漏点:用for-each遍历ArrayList时调用remove,抛出ConcurrentModificationException。为什么漏:实验 5 里如果要“删除所有不及格学生”,直接在循环里list.remove(s)就会翻车。怎么补:用Iterator的remove方法,或者用removeIf:

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

6. 复习清单:六个实验的查漏补缺路线图

复习这套实验代码,最有效的方式不是从头到尾重读,而是按“能力点”逐项自查。下面这份清单按实验顺序排列,每一项都对应一个可验证的代码能力。你可以对着自己的代码逐条打勾,哪一项卡住了,就回到对应实验的代码里找答案。

能力点对应实验自查标准常见问题
类定义与对象创建实验 1能写出带私有属性、构造函数、行为方法的类属性全 public,main 里直接改值
封装与数据校验实验 2setter 方法里有范围校验,不是简单赋值setter 只写this.x = x
继承与 super 调用实验 3子类构造函数显式调用super(...)忘记调用,父类属性未初始化
多态与抽象类实验 4父类引用数组存放子类对象,循环调用用 instanceof 分别处理
接口实现实验 4实现类方法用 public 修饰方法没加 public,编译报错
集合选型实验 5按查找需求选 HashMap,按顺序选 ArrayList一律用 ArrayList,查找靠遍历
异常处理实验 6自定义异常继承 Exception,调用方 try-catch异常被吞掉,没有输出信息
综合程序结构实验 6Main 类只负责交互,业务逻辑在 Manager 类所有代码堆在 main 方法里

复习时建议按这个顺序走:先看实验 1 和 2 的代码,确认封装有没有做到位;再看实验 3 和 4,确认继承体系是否合理、多态有没有真正用起来;最后看实验 5 和 6,确认集合选型和异常处理有没有踩坑。每看完一个实验,把对应的代码默写一遍,重点默写构造函数、重写方法、集合操作和 try-catch 结构。

梳理下来会发现,这套实验代码的真正价值不在于“六个题目”,而在于它把面向对象的三大特性、集合框架、异常处理这些知识点按认知顺序排好了。你不需要额外找练习题,把这六个实验的代码吃透,Java 面向对象的核心考点就全覆盖了。

从那以后我每次复习这套代码,都会先画一张类关系图,把每个实验的类、接口、继承关系标出来,然后再对照代码检查多态和异常处理有没有遗漏。这个习惯帮我省了很多来回翻代码的时间。希望这份梳理也能帮到你。

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

RELATED READING

延伸阅读

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