东北大学Java面向对象实验:从语法到OO思维的六步重构
简介本资源为东北大学《Java面向对象编程基础》课程配套的全部6个实验的完整代码实现面向计算机专业本科生及Java初学者聚焦类与对象、封装、继承、多态、异常处理、集合框架、文件I/O等核心OOP概念的工程化实践。压缩包共124个文件含25个可读性强的.java源码文件覆盖Student、CatalogGUI、GourmetCoffeeGUI等典型实验类、37个.class字节码文件便于直接运行验证、23个.html文档含实验说明与界面展示、11个.xml配置及5个.gif资源图整体仅51KB轻量易解压结构清晰模块间依赖明确适合作为课堂实验参考、课后复现与调试学习。已有1999人学习下载读者可直接获取从需求分析、类设计、接口实现到GUI交互的全流程代码范例尤其适合理解实验中GUI事件监听器如AddModifyListener、DisplayProductListener与数据持久化dat文件的结合方式夯实面向对象建模与工程编码能力。1. 东北大学JAVA面向对象实验1到6不是抄代码是重建OO思维的六次“手术”你手头那份标着“东北大学JAVA面向对象实验全部实验代码1到6”的压缩包大概率不是学习资料而是个“思维矫正器”——它不教你怎么写public class Student { }而是逼你亲手把“学生有姓名、学号、成绩”这种自然语言一锤一钉地锻造成类、属性、方法、继承链和多态调用。我带过三届某高校计算机专业实训班90%的学生第一次交实验2封装与访问控制时会在private int score;后面直接加一句System.out.println(score);然后纳闷为什么编译报错。这六个实验本质是六次渐进式OO建模训练从实验1的类与对象基础定义到实验6的综合应用含接口、异常、集合与简单GUI每一步都在拆解“现实世界如何映射为可运行的Java结构”。它适合刚学完语法、但面对“设计一个银行账户系统”就卡壳的初学者也适合工作两年、写业务逻辑仍靠if-else堆砌、想补上抽象能力缺口的开发者。别急着解压zip——先理解这六次实验在练什么不是语法点罗列而是用最小可行模型反复锤炼“谁该负责什么”“数据该藏在哪”“变化该由谁响应”这三个OO灵魂问题。2. 实验1与2从“创建对象”到“必须封装”的认知断层跨越实验1通常聚焦类定义、对象实例化与基本方法调用看似简单却是OO思维的第一道门槛。实验2则突然引入private/public/protected、getter/setter、构造方法重载等机制很多学生在这里第一次体验“代码能跑通但被老师打回重写”的挫败感。这不是语法刁难而是强制你回答“这个字段到底该不该让外部直接改如果不能那谁来管它怎么管”2.1 实验1用Student类验证“对象即状态行为”的最小闭环常见错误是把Student写成纯数据容器只有字段无方法或把所有逻辑塞进main里。正确做法是让Student自己管理自己的状态变更// Student.java public class Student { private String name; private int id; private double gpa; // 构造方法强制初始化关键状态 public Student(String name, int id) { this.name name; this.id id; this.gpa 0.0; // 默认值避免未初始化 } // 行为方法gpa只能通过addCourse成绩来更新而非直接赋值 public void addCourse(double score) { if (score 0 score 100) { // 简化版GPA计算直接取平均分实际应加权 this.gpa (this.gpa * getCourseCount() score) / (getCourseCount() 1); } } // 辅助方法隐藏内部计数逻辑 private int getCourseCount() { // 实际项目中可能查数据库或List.size()此处简化 return 1; // 演示用真实实验需维护课程列表 } // 提供只读访问 public String getName() { return name; } public int getId() { return id; } public double getGpa() { return gpa; } }逻辑说明addCourse()方法封装了GPA更新规则外部无法绕过此逻辑直接修改gpa。getCourseCount()设为private因为课程数量统计方式属于内部实现细节调用方只需知道“加一门课GPA会重算”无需知道怎么算。参数说明score参数做了范围校验0~100这是实验1就该建立的防御性编程习惯而非留到实验2才补。2.2 实验2用Account类实践“封装即责任边界”的硬约束实验2的核心是理解private不是为了“不让看”而是为了“明确谁对数据负责”。以银行账户为例余额不能被随意篡改但可以被存取操作改变——这个“改变权”必须由Account自己掌控。// Account.java public class Account { private String accountNumber; private double balance; private static int nextId 1000; // 静态字段所有账户共享的ID生成器 // 构造方法强制传入初始余额且必须0 public Account(double initialBalance) { if (initialBalance 0) { throw new IllegalArgumentException(初始余额不能为负数); } this.accountNumber ACC (nextId); this.balance initialBalance; } // 存款余额增加返回新余额体现操作结果 public double deposit(double amount) { if (amount 0) { throw new IllegalArgumentException(存款金额必须大于0); } this.balance amount; return this.balance; } // 取款余额减少但需检查是否透支 public boolean withdraw(double amount) { if (amount 0) { throw new IllegalArgumentException(取款金额必须大于0); } if (amount this.balance) { System.out.println(余额不足取款失败); return false; } this.balance - amount; return true; } // 只读访问外部可查余额但不可改 public double getBalance() { return balance; } public String getAccountNumber() { return accountNumber; } }逻辑说明withdraw()返回boolean而非void是因为取款操作有明确的成功/失败语义调用方需要据此决策如提示用户。deposit()返回新余额方便链式调用或日志记录。nextId用static确保账户编号全局唯一且自增这是静态成员的典型应用场景。参数说明所有金额参数都做了正数校验IllegalArgumentException是实验2要求掌握的异常类型它比System.out.println更能体现“非法输入应中断流程”的OO原则。3. 实验3与4继承不是“抄代码”是建模“is-a”关系的精密手术实验3引入继承extends实验4深化为多态Override、父类引用指向子类对象。学生常犯的错误是为继承而继承比如让Dog继承Car只因两者都有name字段。真正的继承必须满足“is-a”关系狗是一种动物不是一种车且子类要扩展或特化父类行为而非仅仅复用字段。3.1 实验3用Animal体系构建“可扩展的类型家族”实验3的关键是设计合理的父类抽象层级。以Animal为例它不应包含engineType那是车的属性而应聚焦生物共性name、age、makeSound()。// Animal.java父类定义共性与契约 public abstract class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name name; this.age age; } // 抽象方法强制子类实现“发声”行为体现多态基础 public abstract void makeSound(); // 具体方法所有动物共有的行为子类可直接继承 public void sleep() { System.out.println(name 正在睡觉); } // Getter提供受控访问 public String getName() { return name; } public int getAge() { return age; } } // Dog.java子类特化行为 public class Dog extends Animal { private String breed; // 狗特有属性 public Dog(String name, int age, String breed) { super(name, age); // 必须调用父类构造 this.breed breed; } // 重写抽象方法狗的叫声是“汪汪” Override public void makeSound() { System.out.println(name 汪汪叫); } // 新增狗特有方法 public void fetch() { System.out.println(name 正在捡球); } } // Cat.java另一子类不同特化 public class Cat extends Animal { private boolean isIndoor; // 猫特有属性 public Cat(String name, int age, boolean isIndoor) { super(name, age); this.isIndoor isIndoor; } // 重写抽象方法猫的叫声是“喵喵” Override public void makeSound() { System.out.println(name 喵喵叫); } // 新增猫特有方法 public void climb() { System.out.println(name 正在爬树); } }逻辑说明Animal设为abstract因为它代表一个概念不能直接实例化你无法创建一个“动物”只能创建“狗”或“猫”。makeSound()是抽象方法强制子类定义自己的发声逻辑这是多态的基石。sleep()是具体方法所有动物都会睡子类无需重写。breed和isIndoor是子类特有属性体现了继承的“扩展”价值。参数说明子类构造方法中super(name, age)是必须的它确保父类状态被正确初始化。若遗漏编译器会报错这是Java对继承安全性的硬约束。3.2 实验4用多态数组实现“统一处理分别响应”的运行时调度实验4的落地核心是用父类类型声明数组/集合存入不同子类对象再通过统一方法调用触发各自重写的行为。这模拟了真实系统中“插件化”或“策略模式”的雏形。// TestPolymorphism.java测试类 public class TestPolymorphism { public static void main(String[] args) { // 创建多态数组声明为Animal类型但实际存Dog和Cat对象 Animal[] animals new Animal[3]; animals[0] new Dog(旺财, 3, 金毛); animals[1] new Cat(咪咪, 2, true); animals[2] new Dog(小黑, 5, 拉布拉多); // 统一循环调用编译时看Animal类型运行时看实际对象类型 System.out.println( 动物园声音表演 ); for (Animal animal : animals) { animal.makeSound(); // 多态调用旺财汪汪咪咪喵喵小黑汪汪 } System.out.println(\n 动物园休息时间 ); for (Animal animal : animals) { animal.sleep(); // 调用父类具体方法行为一致 } // 进阶向下转型Downcasting调用子类特有方法需谨慎 System.out.println(\n 特色技能展示 ); for (Animal animal : animals) { if (animal instanceof Dog) { Dog dog (Dog) animal; // 安全转型先instanceof检查 dog.fetch(); } else if (animal instanceof Cat) { Cat cat (Cat) animal; cat.climb(); } } } }逻辑说明for (Animal animal : animals)循环中animal.makeSound()的调用在编译时绑定到Animal的抽象方法签名但JVM在运行时根据animal实际指向的对象类型Dog或Cat动态选择对应的makeSound()实现。这就是“动态绑定”Dynamic Binding是多态的灵魂。参数说明instanceof检查是向下转型的安全前提避免ClassCastException。实验4要求掌握此模式因为它模拟了真实场景系统接收一个通用对象需根据其真实类型执行差异化逻辑如不同支付方式的pay()方法。4. 实验5接口不是“高级抽象类”是定义“能做什么”的契约协议实验5引入接口interface学生易混淆接口与抽象类。关键区别在于抽象类描述“是什么”is-a接口描述“能做什么”can-do。一个类可以继承一个父类但能实现多个接口这解决了Java单继承的局限支撑了更灵活的职责分离。4.1 用Flyable与Swimmable接口解耦“飞行”与“游泳”能力实验5的经典案例是给动物添加能力而非修改继承链。例如鸭子既能飞又能游企鹅能游但不能飞飞机能飞但不是动物——用继承强行归类会导致体系臃肿或违反常识。接口完美解决此问题。// Flyable.java能力接口 public interface Flyable { // 接口方法默认public abstract无需写 void fly(); void land(); } // Swimmable.java另一能力接口 public interface Swimmable { void swim(); void dive(); } // Duck.java实现两个接口 public class Duck extends Animal implements Flyable, Swimmable { public Duck(String name, int age) { super(name, age); } Override public void makeSound() { System.out.println(name 嘎嘎叫); } // 实现Flyable Override public void fly() { System.out.println(name 正在拍打翅膀飞行); } Override public void land() { System.out.println(name 安全降落); } // 实现Swimmable Override public void swim() { System.out.println(name 正在水中划水); } Override public void dive() { System.out.println(name 潜入水下捕食); } } // Penguin.java只实现Swimmable public class Penguin extends Animal implements Swimmable { public Penguin(String name, int age) { super(name, age); } Override public void makeSound() { System.out.println(name 嘎嘎叫短促); } Override public void swim() { System.out.println(name 正在高速游泳); } Override public void dive() { System.out.println(name 深潜至百米水下); } } // Airplane.java非动物但能飞 public class Airplane implements Flyable { private String model; public Airplane(String model) { this.model model; } Override public void fly() { System.out.println(model 正在巡航高度飞行); } Override public void land() { System.out.println(model 正在跑道降落); } }逻辑说明Duck同时实现Flyable和Swimmable表明它具备两种独立能力。Penguin只实现Swimmable符合生物学事实。Airplane虽非Animal子类但因实现Flyable可被任何期望“飞行器”的系统接纳。这体现了接口的“契约”本质只要遵守fly()和land()的约定就是合格的飞行器。参数说明接口中方法无需public abstract修饰Java 8默认但实现类中的Override注解是强制的它确保编译器检查方法签名是否匹配避免拼写错误导致静默失败如fliy()。4.2 用Comparator接口定制排序逻辑告别硬编码的if-else比较实验5常结合集合ArrayList与Collections.sort()要求用Comparator实现按不同字段排序。这让学生理解接口不仅是定义能力更是定义“比较规则”的标准协议。import java.util.*; // Student.java添加Comparable实现支持自然排序 public class Student implements ComparableStudent { private String name; private int id; private double gpa; // 构造、getter等略... // 实现Comparable按GPA降序高分在前 Override public int compareTo(Student other) { // Double.compare处理null和精度问题比直接减法安全 return Double.compare(other.gpa, this.gpa); // 降序other在前 } } // NameComparator.java独立Comparator按姓名升序 public class NameComparator implements ComparatorStudent { Override public int compare(Student s1, Student s2) { return s1.getName().compareTo(s2.getName()); // 字符串自然排序 } } // TestSorting.java演示多种排序 public class TestSorting { public static void main(String[] args) { ListStudent students Arrays.asList( new Student(张三, 101, 3.8), new Student(李四, 102, 3.9), new Student(王五, 103, 3.7) ); // 方式1用Comparable自然排序按GPA Collections.sort(students); System.out.println(按GPA排序: students); // 方式2用NameComparator按姓名排序 Collections.sort(students, new NameComparator()); System.out.println(按姓名排序: students); // 方式3用Lambda表达式Java 8更简洁 students.sort((s1, s2) - Integer.compare(s1.getId(), s2.getId())); System.out.println(按学号排序: students); } }逻辑说明Comparable是“自然排序”协议一个类通常只实现一种如按主键。Comparator是“临时排序”协议可针对同一类定义多种排序规则且无需修改类源码。Lambda表达式(s1, s2) - ...是Comparator的函数式接口实现大幅简化代码。参数说明Double.compare(a,b)比a-b安全避免浮点数精度误差导致0.0000001被误判为正数Integer.compare同理防止整数溢出如Integer.MAX_VALUE - (-1)。5. 实验6综合实战——用面向对象重构一个“学生成绩管理系统”实验6是前五个实验的集大成者要求整合类、封装、继承、多态、接口、异常、集合、文件I/O有时含简单GUI。核心目标不是堆砌技术点而是用OO原则解决真实痛点数据持久化、用户交互、业务规则校验。常见翻车点是把所有逻辑塞进一个Main类或用static方法全局操作彻底丢失对象边界。5.1 系统架构设计分层职责拒绝上帝类实验6的成败取决于设计。我推荐三层极简架构Entity层Student、Course、Grade等纯数据类专注状态与基础行为如Student.addGrade(grade)。Service层GradeService、StudentService等业务类协调多个Entity处理核心规则如“同一课程不能重复录入”、“GPA计算”。UI层ConsoleUI或SimpleGUI只负责输入输出不碰业务逻辑。// Grade.javaEntity成绩实体 public class Grade { private String courseId; private String courseName; private double score; public Grade(String courseId, String courseName, double score) { this.courseId courseId; this.courseName courseName; // 校验分数 if (score 0 || score 100) { throw new IllegalArgumentException(成绩必须在0-100之间); } this.score score; } // Getter... public double getScore() { return score; } public String getCourseId() { return courseId; } } // Student.javaEntity增强版内聚成绩管理 import java.util.*; public class Student { private String id; private String name; private ListGrade grades; public Student(String id, String name) { this.id id; this.name name; this.grades new ArrayList(); // 初始化集合避免null } // 内聚方法学生自己管理成绩添加确保不重复 public boolean addGrade(Grade grade) { // 检查是否已存在该课程成绩 for (Grade g : grades) { if (g.getCourseId().equals(grade.getCourseId())) { System.out.println(错误课程 grade.getCourseName() 成绩已存在); return false; } } grades.add(grade); return true; } // 计算GPA封装复杂逻辑 public double calculateGPA() { if (grades.isEmpty()) return 0.0; double sum 0.0; for (Grade g : grades) { sum g.getScore(); } return Math.round((sum / grades.size()) * 100.0) / 100.0; // 保留两位小数 } // Getter... public ListGrade getGrades() { return new ArrayList(grades); } // 返回副本防外部修改 }逻辑说明Student.addGrade()内聚了“去重”逻辑外部调用者无需关心如何检查重复只需传入Grade对象并接收boolean结果。getGrades()返回new ArrayList(grades)而非直接返回grades这是防御性复制Defensive Copy防止外部通过返回的List修改学生私有成绩数据。参数说明Math.round(x * 100.0) / 100.0是安全的四舍五入到两位小数的方法比String.format(%.2f, x)更适合数值计算场景。5.2 GradeService用策略模式解耦GPA计算与显示逻辑实验6常要求支持不同GPA算法如加权GPA、等级制GPA。硬编码if-else会污染Student类。用接口策略模式让算法可插拔。// GPACalculator.java策略接口 public interface GPACalculator { double calculate(ListGrade grades); } // SimpleGPACalculator.java简单平均策略 public class SimpleGPACalculator implements GPACalculator { Override public double calculate(ListGrade grades) { if (grades.isEmpty()) return 0.0; double sum 0.0; for (Grade g : grades) { sum g.getScore(); } return Math.round((sum / grades.size()) * 100.0) / 100.0; } } // WeightedGPACalculator.java加权策略需Course有credit属性 public class WeightedGPACalculator implements GPACalculator { Override public double calculate(ListGrade grades) { // 实验6若未要求课程学分此策略可简化或省略 // 此处仅示意需Grade类扩展credit字段 return 0.0; // 占位 } } // GradeService.java业务服务注入策略 import java.util.*; public class GradeService { private GPACalculator calculator; // 策略依赖 public GradeService(GPACalculator calculator) { this.calculator calculator; } // 计算学生GPA委托给策略自身不持有算法 public double calculateStudentGPA(Student student) { return calculator.calculate(student.getGrades()); } // 批量计算班级GPA public double calculateClassGPA(ListStudent students) { if (students.isEmpty()) return 0.0; double sum 0.0; for (Student s : students) { sum calculateStudentGPA(s); } return Math.round((sum / students.size()) * 100.0) / 100.0; } }逻辑说明GradeService不关心GPA怎么算只负责“找谁算”。通过构造方法注入GPACalculator实现了业务逻辑与算法的解耦。若需求变更为加权GPA只需换一个策略实现GradeService代码零修改。参数说明策略模式的代价是增加类数量但换来的是可维护性。实验6中若题目未明确要求多种算法用SimpleGPACalculator即可不必过度设计。5.3 ConsoleUI用循环菜单驱动保持UI与业务分离最后是交互层。关键原则UI只做三件事——获取用户输入、调用Service方法、打印结果。绝不出现student.setGpa(...)或if (choice 1)处理业务。// ConsoleUI.javaUI层 import java.util.*; public class ConsoleUI { private Scanner scanner; private GradeService gradeService; private ListStudent students; public ConsoleUI() { this.scanner new Scanner(System.in); this.gradeService new GradeService(new SimpleGPACalculator()); this.students new ArrayList(); } public void start() { System.out.println( 东北大学学生成绩管理系统 ); while (true) { showMenu(); int choice getUserChoice(); if (!handleChoice(choice)) break; // 0退出 } System.out.println(感谢使用); } private void showMenu() { System.out.println(\n请选择操作); System.out.println(1. 添加学生); System.out.println(2. 录入成绩); System.out.println(3. 查询学生GPA); System.out.println(4. 显示班级平均GPA); System.out.println(0. 退出); System.out.print(请输入选项); } private int getUserChoice() { try { return Integer.parseInt(scanner.nextLine().trim()); } catch (NumberFormatException e) { System.out.println(输入无效请输入数字); return -1; } } private boolean handleChoice(int choice) { switch (choice) { case 1: addStudent(); break; case 2: enterGrade(); break; case 3: queryStudentGPA(); break; case 4: displayClassGPA(); break; case 0: return false; // 退出循环 default: System.out.println(无效选项请重试); } return true; } private void addStudent() { System.out.print(请输入学生学号); String id scanner.nextLine().trim(); System.out.print(请输入学生姓名); String name scanner.nextLine().trim(); if (id.isEmpty() || name.isEmpty()) { System.out.println(学号和姓名不能为空); return; } // 检查学号是否已存在 for (Student s : students) { if (s.getId().equals(id)) { System.out.println(学号 id 已存在); return; } } students.add(new Student(id, name)); System.out.println(学生添加成功); } private void enterGrade() { System.out.print(请输入学生学号); String id scanner.nextLine().trim(); Student student findStudentById(id); if (student null) { System.out.println(未找到学号为 id 的学生); return; } System.out.print(请输入课程ID); String cid scanner.nextLine().trim(); System.out.print(请输入课程名称); String cname scanner.nextLine().trim(); System.out.print(请输入成绩); try { double score Double.parseDouble(scanner.nextLine().trim()); Grade grade new Grade(cid, cname, score); if (student.addGrade(grade)) { System.out.println(成绩录入成功); } } catch (NumberFormatException | IllegalArgumentException e) { System.out.println(成绩录入失败 e.getMessage()); } } private void queryStudentGPA() { System.out.print(请输入学生学号); String id scanner.nextLine().trim(); Student student findStudentById(id); if (student null) { System.out.println(未找到学号为 id 的学生); return; } double gpa gradeService.calculateStudentGPA(student); System.out.printf(学生 %s (%s) 的GPA为%.2f\n, student.getName(), student.getId(), gpa); } private void displayClassGPA() { double classGPA gradeService.calculateClassGPA(students); System.out.printf(班级平均GPA为%.2f\n, classGPA); } private Student findStudentById(String id) { for (Student s : students) { if (s.getId().equals(id)) { return s; } } return null; } }逻辑说明ConsoleUI中所有System.out.println和scanner.nextLine()都集中在UI层GradeService和Student完全不知晓控制台存在。findStudentById()是UI层的辅助方法不暴露给业务层。addStudent()中检查学号重复是UI层的数据校验与Student类的封装不冲突Student管自己状态UI管输入合法性。参数说明scanner.nextLine().trim()去除首尾空格避免 101 被误判为无效学号。try-catch包裹Double.parseDouble捕获NumberFormatException这是实验6要求的异常处理实践。6. 避坑指南六个实验里最常踩的5个“血泪坑”及后悔药面向对象实验的坑往往不在语法而在思维惯性。以下是我在批改数百份实验报告后总结的最高频、最致命的5个坑每个都附带现象、根因和可立即生效的“后悔药”。6.1 坑1在子类构造方法中忘记调用super()导致父类字段为null或0现象Dog d new Dog(旺财,3,金毛);创建后d.getName()返回nulld.getAge()返回0但d.getBreed()正常。原因Dog构造方法中未写super(name, age)Java自动插入super()无参父类构造而Animal无参构造未初始化name和age导致它们保持默认值null和0。后悔药在子类构造方法第一行必须显式调用super(...)传入父类所需参数。若父类无无参构造编译器会强制你写super这是Java的保护机制。6.2 坑2用比较字符串导致“用户名登录失败”现象if (inputName admin)永远为false即使用户输入了admin。原因比较的是对象内存地址admin字面量和scanner.nextLine()返回的字符串是两个不同对象地址不同。String的值比较必须用.equals()。后悔药所有字符串内容比较无条件用.equals()。为防null写成admin.equals(inputName)更安全常量在前避免inputName.equals(admin)在inputName为null时抛NullPointerException。6.3 坑3在ArrayList中用for循环删除元素导致漏删或IndexOutOfBoundsException现象遍历ArrayListGrade删除所有低于60分的成绩结果只删了一半或程序崩溃。原因list.remove(i)后后续元素前移i导致跳过下一个元素或i超出新长度引发异常。后悔药删除集合元素必须用迭代器IteratorIteratorGrade it grades.iterator(); while (it.hasNext()) { if (it.next().getScore() 60) { it.remove(); // 安全删除 } }或用Java 8的removeIf()grades.removeIf(g - g.getScore() 60);6.4 坑4将集合List/Map作为方法参数直接返回导致外部可篡改内部状态现象Student.getGrades()返回this.grades外部调用student.getGrades().clear()后学生所有成绩消失。原因返回了私有集合的引用破坏了封装性。后悔药永远返回集合的副本Defensive Copypublic ListGrade getGrades() { return new ArrayList(this.grades); // 不是 this.grades }对于不可变需求可返回Collections.unmodifiableList(this.grades)但需注意其包装的List若被内部修改仍会反映。6.5 坑5在多态数组中试图调用子类特有方法而不转型编译报错现象Animal[] animals {new Dog(), new Cat()}; animals[0].fetch();编译失败提示Animal中没有fetch()方法。原因编译器只认变量声明类型Animal不认实际对象类型Dog。fetch()是Dog特有方法Animal引用无法访问。后悔药必须先instanceof检查再安全转型if (animals[0] instanceof Dog) { ((Dog) animals[0]).fetch(); // 向下转型后调用 }更优雅的方案是在父类Animal中定义通用方法如performTrick()由子类重写避免转型。7. 最后一课用UML类图反向验证你的代码是否真OO做完六个实验别急着交代码。拿出一张纸用最原始的UML类图不用工具手画检验你的代码是否真的践行了OO。这不是形式主义而是把隐性思维显性化的“照妖镜”。我坚持用这个方法自查十年它帮我揪出过无数“伪面向对象”代码。7.1 画图三步法从代码到图形的本文还有配套的精品资源点击获取