|
|
@@ -0,0 +1,744 @@
|
|
|
+# 2026-07-24 多态(Polymorphism)基础练习(课堂练习用)
|
|
|
+
|
|
|
+> **说明**:以下习题围绕今日授课核心内容——**多态(Polymorphism)** 展开,涵盖多态的概念与前提、多态成员访问特点、向上转型/向下转型、instanceof 运算符、多态与抽象类/接口的结合、接口默认方法/静态方法以及模板方法设计模式。
|
|
|
+>
|
|
|
+> 题目分为三部分:**随堂练习**(多态基础,4 题)、**进阶练习**(综合挑战,2 题)和 **课后作业**(接口默认方法/静态方法 + 模板方法,2 题)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 第一部分:随堂练习
|
|
|
+
|
|
|
+> 以下练习围绕多态的基础概念和用法,由浅入深。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 练习 1:多态入门——Pet 宠物喂食体系
|
|
|
+
|
|
|
+**难度**:⭐
|
|
|
+**知识点**:多态概念、多态三个前提条件、父类引用指向子类对象
|
|
|
+
|
|
|
+创建一个宠物喂食体系,体验 **同一个方法调用,不同子类表现出不同的行为**。
|
|
|
+
|
|
|
+**`Pet` 类**(父类):
|
|
|
+- **普通方法**:`void eat()` —— 输出 `"宠物在吃饭..."`
|
|
|
+
|
|
|
+**`Dog` 类**(继承 Pet):
|
|
|
+- **重写** `eat()` —— 输出 `"小狗在啃骨头..."`
|
|
|
+
|
|
|
+**`Cat` 类**(继承 Pet):
|
|
|
+- **重写** `eat()` —— 输出 `"小猫在吃鱼..."`
|
|
|
+
|
|
|
+**测试类 `PetTest` 要求**:
|
|
|
+
|
|
|
+```java
|
|
|
+public class PetTest {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ // 1. 使用父类引用指向子类对象(多态)
|
|
|
+ Pet p1 = new Dog();
|
|
|
+ Pet p2 = new Cat();
|
|
|
+
|
|
|
+ // 2. 调用 eat() 方法——运行期执行子类的重写方法
|
|
|
+ p1.eat();
|
|
|
+ p2.eat();
|
|
|
+
|
|
|
+ // 3. 思考:如果 Pet pet = new Pet(); 可以吗?使用 new Pet() 有什么缺点?
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**预期输出**:
|
|
|
+```
|
|
|
+小狗在啃骨头...
|
|
|
+小猫在吃鱼...
|
|
|
+```
|
|
|
+
|
|
|
+**思考题**:
|
|
|
+1. 以上代码体现了多态的哪个前提条件?
|
|
|
+2. 如果 `eat()` 方法没有被重写,输出会是什么?为什么?
|
|
|
+3. 如果 `Pet` 类中还有 `sleep()` 方法但 `Dog` 和 `Cat` 没有重写它,通过 `p1.sleep()` 调用会执行哪个类的 `sleep()`?
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 练习 2:多态成员访问特点——理解「编译看左边,运行看右边」
|
|
|
+
|
|
|
+**难度**:⭐⭐
|
|
|
+**知识点**:多态中成员变量和成员方法的访问特点、编译期 vs 运行期
|
|
|
+
|
|
|
+通过代码验证多态的成员访问规则。
|
|
|
+
|
|
|
+**`交通工具` 体系**:
|
|
|
+
|
|
|
+**`Vehicle` 类**(父类):
|
|
|
+- **成员变量**:`String name = "交通工具"`
|
|
|
+- **普通方法**:`void run()` —— 输出 `"交通工具在行驶..."`
|
|
|
+- **普通方法**:`void stop()` —— 输出 `"交通工具刹车停止"`
|
|
|
+- **静态方法**:`static void info()` —— 输出 `"Vehicle 静态方法"`
|
|
|
+
|
|
|
+**`Car` 类**(继承 Vehicle):
|
|
|
+- **成员变量**:`String name = "小汽车"`
|
|
|
+- **重写** `run()` —— 输出 `"小汽车在高速行驶..."`
|
|
|
+- **特有方法**:`void openSunroof()` —— 输出 `"打开天窗~"`
|
|
|
+
|
|
|
+**测试类要求**:
|
|
|
+
|
|
|
+编写 `VehicleTest` 类,创建一个 `Car` 对象,分别使用「父类引用指向子类对象」和「子类引用指向子类对象」两种方式,观察成员变量、成员方法、特有方法的访问差异。
|
|
|
+
|
|
|
+**模板代码**:
|
|
|
+```java
|
|
|
+public class VehicleTest {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ // 情况一:父类引用指向子类对象(多态)
|
|
|
+ Vehicle v = new Car();
|
|
|
+ System.out.println(v.name); // 输出什么?
|
|
|
+ v.run(); // 输出什么?
|
|
|
+ v.stop(); // 输出什么?
|
|
|
+ // v.openSunroof(); // 能否调用?编译能通过吗?
|
|
|
+
|
|
|
+ // 情况二:子类引用指向子类对象(普通)
|
|
|
+ Car c = new Car();
|
|
|
+ System.out.println(c.name); // 输出什么?
|
|
|
+ c.run(); // 输出什么?
|
|
|
+ c.openSunroof(); // 能否调用?
|
|
|
+
|
|
|
+ // 情况三:静态方法
|
|
|
+ v.info(); // 调用谁的静态方法?
|
|
|
+ c.info(); // 调用谁的静态方法?
|
|
|
+ Vehicle.info(); // 推荐哪种调用方式?
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**请写出你的预期输出**(先思考,再运行验证):
|
|
|
+```
|
|
|
+v.name → __________
|
|
|
+v.run() → __________
|
|
|
+v.stop() → __________
|
|
|
+c.name → __________
|
|
|
+c.run() → __________
|
|
|
+v.info() → __________
|
|
|
+```
|
|
|
+
|
|
|
+**总结填空**:
|
|
|
+1. 成员变量:编译看______,运行看______
|
|
|
+2. 成员方法(非静态):编译看______,运行看______
|
|
|
+3. 静态方法:编译看______,运行看______(因为静态方法属于______,不参与多态)
|
|
|
+4. 通过父类引用,______(能/不能)调用子类的特有方法
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 练习 3:向下转型 + instanceof——动物园管理员
|
|
|
+
|
|
|
+**难度**:⭐⭐⭐
|
|
|
+**知识点**:向上转型、向下转型、instanceof 运算符、子类特有方法的调用
|
|
|
+
|
|
|
+**场景描述**:动物园里有多种动物,管理员需要根据不同的动物执行不同的操作。有些动物会游泳,有些动物会飞翔,这些是特有行为。
|
|
|
+
|
|
|
+**`Animal` 类**(父类):
|
|
|
+- **普通方法**:`void eat()` —— 输出 `"动物在吃东西"`
|
|
|
+- **普通方法**:`void sleep()` —— 输出 `"动物在睡觉"`
|
|
|
+
|
|
|
+**`Fish` 类**(继承 Animal):
|
|
|
+- **重写** `eat()` —— 输出 `"鱼儿在吃饲料"`
|
|
|
+- **重写** `sleep()` —— 输出 `"鱼儿睁着眼睛睡觉"`
|
|
|
+- **特有方法**:`void swim()` —— 输出 `"鱼儿在水中游来游去~"`
|
|
|
+
|
|
|
+**`Bird` 类**(继承 Animal):
|
|
|
+- **重写** `eat()` —— 输出 `"小鸟在啄米"`
|
|
|
+- **重写** `sleep()` —— 输出 `"小鸟在巢穴里睡觉"`
|
|
|
+- **特有方法**:`void fly()` —— 输出 `"小鸟在天空中飞翔~"`
|
|
|
+
|
|
|
+**请完成 `ZooKeeper` 类**:
|
|
|
+
|
|
|
+```java
|
|
|
+public class ZooKeeper {
|
|
|
+ // 这个方法接收 Animal 类型的参数(多态)
|
|
|
+ // 要求:
|
|
|
+ // 1. 调用 eat() 和 sleep()
|
|
|
+ // 2. 使用 instanceof 判断是哪种动物
|
|
|
+ // 3. 如果是 Fish,向下转型并调用 swim()
|
|
|
+ // 4. 如果是 Bird,向下转型并调用 fly()
|
|
|
+ public void handleAnimal(Animal animal) {
|
|
|
+ // 请补充代码
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ public static void main(String[] args) {
|
|
|
+ ZooKeeper keeper = new ZooKeeper();
|
|
|
+
|
|
|
+ // 使用向上转型创建对象
|
|
|
+ Animal a1 = new Fish();
|
|
|
+ Animal a2 = new Bird();
|
|
|
+
|
|
|
+ // 交给管理员处理
|
|
|
+ keeper.handleAnimal(a1);
|
|
|
+ keeper.handleAnimal(a2);
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**预期输出**:
|
|
|
+```
|
|
|
+鱼儿在吃饲料
|
|
|
+鱼儿睁着眼睛睡觉
|
|
|
+鱼儿在水中游来游去~
|
|
|
+小鸟在啄米
|
|
|
+小鸟在巢穴里睡觉
|
|
|
+小鸟在天空中飞翔~
|
|
|
+```
|
|
|
+
|
|
|
+**思考题**:
|
|
|
+1. 如果调用 `keeper.handleAnimal(new Animal())` 会发生什么?`instanceof` 判断会进入哪个分支?
|
|
|
+2. 如果去掉 `instanceof` 判断,直接强制转型 `(Fish) animal`,当传入 `Bird` 对象时会怎样?
|
|
|
+3. 为什么需要向下转型?向上转型后直接调用 `animal.swim()` 可以吗?为什么?
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 练习 4:判断多态题型——哪些属于多态应用
|
|
|
+
|
|
|
+**难度**:⭐⭐
|
|
|
+**知识点**:多态的判断、多态与普通继承的区别
|
|
|
+
|
|
|
+判断以下代码片段中,**哪些体现了多态**,哪些没有,并说明理由。
|
|
|
+
|
|
|
+| 序号 | 代码片段 | 是否多态? | 理由 |
|
|
|
+|------|---------|-----------|------|
|
|
|
+| 1 | `Animal a = new Dog(); a.eat();` | | |
|
|
|
+| 2 | `Dog d = new Dog(); d.eat();` | | |
|
|
|
+| 3 | `Usb usb = new Mouse(); noteBook.useUsb(usb);` | | |
|
|
|
+| 4 | `Teacher t = new Teacher(); t.play();` | | |
|
|
|
+| 5 | `Person p = new Student(); ((Student)p).study();` | | |
|
|
|
+| 6 | `Object obj = "Hello";` | | |
|
|
|
+
|
|
|
+**拓展思考**:
|
|
|
+```java
|
|
|
+// 以下代码是否是多态?为什么?
|
|
|
+public void feed(Pet pet) {
|
|
|
+ pet.eat();
|
|
|
+}
|
|
|
+
|
|
|
+// 调用
|
|
|
+feed(new Dog());
|
|
|
+feed(new Cat());
|
|
|
+```
|
|
|
+
|
|
|
+**参考答案思路**:
|
|
|
+
|
|
|
+| 序号 | 是否多态? | 理由 |
|
|
|
+|------|-----------|------|
|
|
|
+| 1 | ✅ 是 | 父类引用指向子类对象,调用重写方法 —— 满足多态三前提 |
|
|
|
+| 2 | ❌ 否 | 子类引用指向子类对象,没有「父类引用指向子类对象」 |
|
|
|
+| 3 | ✅ 是 | 接口引用指向实现类对象,属于接口多态 |
|
|
|
+| 4 | ❌ 否 | 没有父类引用,没有方法重写的调用 |
|
|
|
+| 5 | ✅ 是 | 先向上转型(多态),再向下转型恢复子类完整形态 |
|
|
|
+| 6 | ✅ 是 | String 是 Object 的子类,属于继承多态 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 第二部分:进阶练习
|
|
|
+
|
|
|
+> 以下为综合挑战题,融合多态、向上转型/向下转型、instanceof、对象数组和排序等知识点。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 1 题:多态 + 对象数组 + instanceof——员工绩效管理系统 ⭐⭐⭐⭐
|
|
|
+
|
|
|
+### 需求说明
|
|
|
+
|
|
|
+某公司有三种类型的员工:**正式员工**(全职)、**兼职员工**(按时计薪)和 **实习生**(固定津贴)。每种员工的薪资计算方式不同,且各有特有行为。
|
|
|
+
|
|
|
+使用 **多态数组** 存储所有员工,遍历时通过 `instanceof` 判断类型并向下转型,调用特有方法。
|
|
|
+
|
|
|
+### 类设计
|
|
|
+
|
|
|
+**`Employee` 抽象类**(父类):
|
|
|
+- **私有属性**:`id`(工号,int)、`name`(姓名,String)
|
|
|
+- **有参构造方法**:`Employee(int id, String name)`
|
|
|
+- **抽象方法**:`double calculateSalary()` —— 计算薪资
|
|
|
+- **普通方法**:`void showInfo()` —— 输出 `"工号:xxx,姓名:xxx,薪资:xxx"`(调用 `calculateSalary()` 获得薪资)
|
|
|
+
|
|
|
+**`FullTimeEmployee` 类**(继承 Employee):
|
|
|
+- **私有属性**:`baseSalary`(基本工资,double)、`bonus`(绩效奖金,double)
|
|
|
+- **构造方法**:`FullTimeEmployee(int id, String name, double baseSalary, double bonus)`
|
|
|
+- **重写** `calculateSalary()` —— 返回 `baseSalary + bonus * 1.2`(绩效上浮 20%)
|
|
|
+- **特有方法**:`void attendMeeting()` —— 输出 `"正式员工 xxx 参加周会..."`(xxx 为姓名)
|
|
|
+
|
|
|
+**`PartTimeEmployee` 类**(继承 Employee):
|
|
|
+- **私有属性**:`hourlyRate`(时薪,double)、`hoursWorked`(工作小时数,int)
|
|
|
+- **构造方法**:`PartTimeEmployee(int id, String name, double hourlyRate, int hoursWorked)`
|
|
|
+- **重写** `calculateSalary()` —— 返回 `hourlyRate * hoursWorked`
|
|
|
+- **特有方法**:`void clockIn()` —— 输出 `"兼职员工 xxx 打卡上班..."`(xxx 为姓名)
|
|
|
+
|
|
|
+**`Intern` 类**(继承 Employee):
|
|
|
+- **私有属性**:`allowance`(实习津贴,double)
|
|
|
+- **构造方法**:`Intern(int id, String name, double allowance)`
|
|
|
+- **重写** `calculateSalary()` —— 返回 `allowance`(固定津贴,无其他扣减)
|
|
|
+- **特有方法**:`void learnSkill()` —— 输出 `"实习生 xxx 在学习新技能..."`(xxx 为姓名)
|
|
|
+
|
|
|
+### 测试类要求
|
|
|
+
|
|
|
+**`EmployeeManager` 类**:
|
|
|
+
|
|
|
+1. **创建员工数组**:创建一个 `Employee[]` 数组,包含至少 5 名员工(各类型混合)
|
|
|
+
|
|
|
+ | 类型 | id | 姓名 | 参数 |
|
|
|
+ |------|----|------|------|
|
|
|
+ | FullTimeEmployee | 1001 | 张三 | 基本工资 8000,绩效 2000 |
|
|
|
+ | PartTimeEmployee | 2001 | 李四 | 时薪 50,工作 80 小时 |
|
|
|
+ | Intern | 3001 | 王五 | 津贴 2000 |
|
|
|
+ | FullTimeEmployee | 1002 | 赵六 | 基本工资 10000,绩效 3000 |
|
|
|
+ | PartTimeEmployee | 2002 | 孙七 | 时薪 45,工作 60 小时 |
|
|
|
+
|
|
|
+2. **遍历数组输出基本信息**:调用每个元素的 `showInfo()` 方法
|
|
|
+
|
|
|
+3. **遍历数组,按类型执行特有行为**:
|
|
|
+ - 如果是 `FullTimeEmployee` → 向下转型 → 调用 `attendMeeting()`
|
|
|
+ - 如果是 `PartTimeEmployee` → 向下转型 → 调用 `clockIn()`
|
|
|
+ - 如果是 `Intern` → 向下转型 → 调用 `learnSkill()`
|
|
|
+
|
|
|
+4. **计算总薪资成本**:遍历数组,累加所有员工的 `calculateSalary()`,输出总薪资
|
|
|
+
|
|
|
+5. **查找薪资最高的员工**:遍历数组,找出 `calculateSalary()` 最大的员工,输出其信息
|
|
|
+
|
|
|
+### 思考题
|
|
|
+
|
|
|
+1. `Employee[]` 数组中存储的是不同类型对象,这体现了面向对象哪个特征?
|
|
|
+2. 如果不用 `instanceof` 判断类型,直接向下转型会有什么风险?
|
|
|
+3. 如果新增一个 `Contractor`(外包人员)类,需要修改哪些代码?这些修改是否符合「开闭原则」?
|
|
|
+
|
|
|
+### 预期输出(参考)
|
|
|
+
|
|
|
+```
|
|
|
+========== 员工薪资信息 ==========
|
|
|
+工号:1001,姓名:张三,薪资:10400.00
|
|
|
+工号:2001,姓名:李四,薪资:4000.00
|
|
|
+工号:3001,姓名:王五,薪资:2000.00
|
|
|
+工号:1002,姓名:赵六,薪资:13600.00
|
|
|
+工号:2002,姓名:孙七,薪资:2700.00
|
|
|
+
|
|
|
+========== 执行特有行为 ==========
|
|
|
+正式员工 张三 参加周会...
|
|
|
+兼职员工 李四 打卡上班...
|
|
|
+实习生 王五 在学习新技能...
|
|
|
+正式员工 赵六 参加周会...
|
|
|
+兼职员工 孙七 打卡上班...
|
|
|
+
|
|
|
+========== 薪资统计 ==========
|
|
|
+总薪资成本:32700.00 元
|
|
|
+薪资最高的员工:工号:1002,姓名:赵六,薪资:13600.00
|
|
|
+```
|
|
|
+
|
|
|
+> **提示**:
|
|
|
+> - 数组类型声明为 `Employee[]`,存储不同子类对象——这是多态的数组应用
|
|
|
+> - 使用 `instanceof` 判断具体类型后再向下转型,是安全调用特有方法的标准做法
|
|
|
+> - 薪资计算使用 `double` 类型,输出时保留两位小数使用 `System.out.printf("%.2f", value)`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 2 题:综合应用——Usb 设备管理器(多态 + 接口 + instanceof)⭐⭐⭐⭐⭐
|
|
|
+
|
|
|
+### 需求说明
|
|
|
+
|
|
|
+基于课堂上的 Usb 接口案例,扩展实现一个 Usb 设备管理器。要求:
|
|
|
+
|
|
|
+- `NoteBook` 类可以接入多个 Usb 设备(使用 Usb 数组存储)
|
|
|
+- 不同 Usb 设备有不同的特有功能
|
|
|
+- 使用 `instanceof` 判断设备类型,调用特有方法
|
|
|
+
|
|
|
+### 类设计
|
|
|
+
|
|
|
+**`Usb` 接口**(复用课堂代码):
|
|
|
+- **抽象方法**:`void startUsb()` —— 启动设备
|
|
|
+- **抽象方法**:`void stopUsb()` —— 停止设备
|
|
|
+
|
|
|
+**`Mouse` 类**(实现 Usb):
|
|
|
+- **私有属性**:`brand`(品牌,String)
|
|
|
+- **构造方法**:`Mouse(String brand)`
|
|
|
+- **实现** `startUsb()` —— 输出 `"鼠标已连接(品牌:xxx)"`
|
|
|
+- **实现** `stopUsb()` —— 输出 `"鼠标已断开"`
|
|
|
+- **特有方法**:`void click()` —— 输出 `"鼠标单击——嗒"`
|
|
|
+
|
|
|
+**`Keyboard` 类**(实现 Usb):
|
|
|
+- **私有属性**:`layout`(键盘布局,String,如 "QWERTY" / "AZERTY")
|
|
|
+- **构造方法**:`Keyboard(String layout)`
|
|
|
+- **实现** `startUsb()` —— 输出 `"键盘已连接(布局:xxx)"`
|
|
|
+- **实现** `stopUsb()` —— 输出 `"键盘已断开"`
|
|
|
+- **特有方法**:`void type()` —— 输出 `"键盘输入:Hello World!"`
|
|
|
+
|
|
|
+**`USBHub` 类**(实现 Usb,**拓展新设备类型**):
|
|
|
+- **私有属性**:`portCount`(USB 接口数量,int)
|
|
|
+- **构造方法**:`USBHub(int portCount)`
|
|
|
+- **实现** `startUsb()` —— 输出 `"USB 扩展坞已连接(xxx 个接口)"`
|
|
|
+- **实现** `stopUsb()` —— 输出 `"USB 扩展坞已断开"`
|
|
|
+- **特有方法**:`void expandPorts()` —— 输出 `"扩展出 xxx 个 USB 接口,可连接更多设备"`
|
|
|
+
|
|
|
+**`NoteBook` 类**:
|
|
|
+- **私有属性**:`brand`(品牌,String)、`ports`(Usb 设备数组,最多 4 个)、`portIndex`(当前已插入设备数)
|
|
|
+- **构造方法**:`NoteBook(String brand)` —— 初始化品牌,创建大小为 4 的 Usb 数组
|
|
|
+- **方法**:`void plugIn(Usb device)` —— 插入 Usb 设备到 `ports` 数组
|
|
|
+- **方法**:`void workAll()` —— 遍历 `ports` 数组,对每个设备:
|
|
|
+ 1. 调用 `startUsb()`
|
|
|
+ 2. 使用 `instanceof` 判断设备类型,向下转型调用特有方法
|
|
|
+ - 如果是 `Mouse` → 调用 `click()`
|
|
|
+ - 如果是 `Keyboard` → 调用 `type()`
|
|
|
+ - 如果是 `USBHub` → 调用 `expandPorts()`
|
|
|
+ 3. 调用 `stopUsb()`
|
|
|
+
|
|
|
+### 测试类要求
|
|
|
+
|
|
|
+**`UsbManagerTest` 类**:
|
|
|
+
|
|
|
+1. 创建 `NoteBook` 对象("ThinkPad")
|
|
|
+2. 创建多个 Usb 设备:
|
|
|
+ - `Mouse`("罗技")
|
|
|
+ - `Keyboard`("QWERTY")
|
|
|
+ - `USBHub`(4)
|
|
|
+ - `Mouse`("雷蛇")
|
|
|
+3. 将设备依次插入笔记本
|
|
|
+4. 调用 `workAll()` 让所有设备工作一轮
|
|
|
+
|
|
|
+### 预期输出
|
|
|
+
|
|
|
+```
|
|
|
+========== ThinkPad 笔记本 Usb 设备工作 ==========
|
|
|
+鼠标已连接(品牌:罗技)
|
|
|
+鼠标单击——嗒
|
|
|
+鼠标已断开
|
|
|
+
|
|
|
+键盘已连接(布局:QWERTY)
|
|
|
+键盘输入:Hello World!
|
|
|
+键盘已断开
|
|
|
+
|
|
|
+USB 扩展坞已连接(4 个接口)
|
|
|
+扩展出 4 个 USB 接口,可连接更多设备
|
|
|
+USB 扩展坞已断开
|
|
|
+
|
|
|
+鼠标已连接(品牌:雷蛇)
|
|
|
+鼠标单击——嗒
|
|
|
+鼠标已断开
|
|
|
+```
|
|
|
+
|
|
|
+### 思考题
|
|
|
+
|
|
|
+1. `NoteBook` 类中的 `workAll()` 方法,参数和返回值的类型设计体现了什么原则?
|
|
|
+2. 如果要新增一个 `Printer`(打印机)Usb 设备,需要修改 `NoteBook` 类的代码吗?这符合哪个设计原则?
|
|
|
+3. 对照课堂的 `Test02.java`,本练习增加了什么功能?(提示:从单个设备到多个设备 + instanceof 判断)
|
|
|
+
|
|
|
+### 代码填空挑战
|
|
|
+
|
|
|
+请补全 `NoteBook` 类的 `workAll()` 方法:
|
|
|
+
|
|
|
+```java
|
|
|
+public void workAll() {
|
|
|
+ System.out.println("========== " + brand + " 笔记本 Usb 设备工作 ==========");
|
|
|
+ for (int i = 0; i < portIndex; i++) {
|
|
|
+ Usb device = ports[i];
|
|
|
+ device.startUsb();
|
|
|
+
|
|
|
+ // 请使用 instanceof + 向下转型 调用特有方法
|
|
|
+ // 你的代码:
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ device.stopUsb();
|
|
|
+ System.out.println();
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+# 第三部分:课后作业
|
|
|
+
|
|
|
+> 以下作业围绕接口 JDK8 新特性(默认方法、静态方法)和模板方法设计模式,结合课堂通知系统案例进行扩展。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 作业 1:接口默认方法 + 静态方法——消息通知系统扩展
|
|
|
+
|
|
|
+**难度**:⭐⭐⭐
|
|
|
+**知识点**:接口默认方法(default)、接口静态方法(static)、implements 实现接口、方法重写
|
|
|
+
|
|
|
+### 需求说明
|
|
|
+
|
|
|
+在课堂 `Notifiable` 通知系统基础上,增加更多的通知方式。要求使用 **接口默认方法** 提供通用的日志记录和参数校验功能,使用 **接口静态方法** 提供工具方法。
|
|
|
+
|
|
|
+### 接口定义
|
|
|
+
|
|
|
+**`Notifiable` 接口**(可直接复用课堂代码,或自行重新定义):
|
|
|
+- **抽象方法**:`void doSend(String recipient, String message)`
|
|
|
+- **默认方法**:`default boolean validateParams(...)` —— 校验参数
|
|
|
+- **默认方法**:`default void log(String level, String msg)` —— 记录日志
|
|
|
+- **默认方法**:`default void send(String recipient, String message)` —— 模板方法(固定发送流程)
|
|
|
+- **静态方法**:`static void printNotificationType()` —— 输出系统信息
|
|
|
+
|
|
|
+### 要求实现以下 3 个通知类
|
|
|
+
|
|
|
+**`SMSNotifier` 类**(实现 Notifiable):
|
|
|
+- **重写** `doSend()` —— 输出 `"发送短信给 xxx:内容 -xxx"`
|
|
|
+- **不重写** `log()` —— 使用接口默认实现
|
|
|
+- **不重写** `validateParams()` —— 使用接口默认实现
|
|
|
+
|
|
|
+**`WeChatNotifier` 类**(实现 Notifiable):
|
|
|
+- **私有属性**:`appId`(应用 ID,String)
|
|
|
+- **构造方法**:`WeChatNotifier(String appId)`
|
|
|
+- **重写** `doSend()` —— 输出 `"发送微信消息给 xxx(appId: xxx):内容 -xxx"`
|
|
|
+- **重写** `log()` —— 输出 `"[微信][xxx]消息:xxx"`(添加 [微信] 前缀)
|
|
|
+
|
|
|
+**`StationNotifier` 类**(实现 Notifiable,**站内信通知**):
|
|
|
+- **私有属性**:`platformName`(平台名称,String)
|
|
|
+- **构造方法**:`StationNotifier(String platformName)`
|
|
|
+- **重写** `doSend()` —— 输出 `"[xxx] 站内信发送给 xxx:内容 -xxx"`
|
|
|
+- **重写** `log()` —— 输出 `"[站内信][xxx]消息:xxx"`
|
|
|
+
|
|
|
+### 测试类要求
|
|
|
+
|
|
|
+**`NotificationTest` 类**:
|
|
|
+
|
|
|
+```java
|
|
|
+public class NotificationTest {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ // 1. 调用接口静态方法
|
|
|
+ Notifiable.printNotificationType();
|
|
|
+
|
|
|
+ // 2. 创建不同通知方式的对象
|
|
|
+ Notifiable sms = new SMSNotifier();
|
|
|
+ Notifiable wechat = new WeChatNotifier("wx_app_001");
|
|
|
+ Notifiable station = new StationNotifier("企业办公平台");
|
|
|
+
|
|
|
+ // 3. 发送通知
|
|
|
+ System.out.println("--- 短信通知 ---");
|
|
|
+ sms.send("13800138000", "您的验证码是 123456");
|
|
|
+
|
|
|
+ System.out.println("--- 微信通知 ---");
|
|
|
+ wechat.send("user_openid_123", "您有一条新消息");
|
|
|
+
|
|
|
+ System.out.println("--- 站内信通知 ---");
|
|
|
+ station.send("全体员工", "下午 3 点会议室开会");
|
|
|
+
|
|
|
+ // 4. 测试空参数——触发校验失败
|
|
|
+ System.out.println("--- 空参数测试 ---");
|
|
|
+ sms.send("", "test");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 预期输出
|
|
|
+
|
|
|
+```
|
|
|
+通知系统 v1.0——支持邮件/短信/微信通知
|
|
|
+--- 短信通知 ---
|
|
|
+参数校验通过
|
|
|
+发送短信给 13800138000:内容 -您的验证码是 123456
|
|
|
+[INFO]消息:发送成功
|
|
|
+--- 发送流程结束 ---
|
|
|
+--- 微信通知 ---
|
|
|
+参数校验通过
|
|
|
+发送微信消息给 user_openid_123(appId: wx_app_001):内容 -您有一条新消息
|
|
|
+[微信][INFO]消息:发送成功
|
|
|
+--- 发送流程结束 ---
|
|
|
+--- 站内信通知 ---
|
|
|
+参数校验通过
|
|
|
+[企业办公平台] 站内信发送给 全体员工:内容 -下午 3 点会议室开会
|
|
|
+[站内信][INFO]消息:发送成功
|
|
|
+--- 发送流程结束 ---
|
|
|
+--- 空参数测试 ---
|
|
|
+参数校验失败:收件人或消息不能为空
|
|
|
+```
|
|
|
+
|
|
|
+### 思考题
|
|
|
+
|
|
|
+1. `SMSNotifier` 没有重写 `log()` 方法,调用时执行的是谁的 `log()`?
|
|
|
+2. 接口静态方法 `printNotificationType()` 能通过实现类对象调用吗?如 `sms.printNotificationType()` —— 尝试后说说你的发现。
|
|
|
+3. 如果 `Notifiable` 新增一个默认方法 `default void sendWithRetry(...)`,已有的 3 个实现类需要修改吗?这体现了接口默认方法的什么优势?
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 作业 2:模板方法设计模式——数据备份系统
|
|
|
+
|
|
|
+**难度**:⭐⭐⭐⭐
|
|
|
+**知识点**:模板方法设计模式、接口默认方法、抽象方法与默认方法的配合
|
|
|
+
|
|
|
+### 需求说明
|
|
|
+
|
|
|
+开发一个数据备份系统,所有备份任务都遵循相同的流程,但不同的备份方式(数据库备份、文件备份、云备份)在具体执行步骤上有所不同。
|
|
|
+
|
|
|
+**核心流程**(模板方法定义):
|
|
|
+1. **校验参数**(所有备份相同)—— 检查备份路径/配置是否合法
|
|
|
+2. **连接资源**(不同备份方式不同)← **抽象方法**
|
|
|
+3. **执行备份**(不同备份方式不同)← **抽象方法**
|
|
|
+4. **记录日志**(所有备份相同)—— 记录备份结果
|
|
|
+5. **关闭资源**(不同备份方式不同)← **抽象方法**
|
|
|
+
|
|
|
+### 使用接口 + 默认方法实现模板方法模式
|
|
|
+
|
|
|
+**`Backupable` 接口**:
|
|
|
+
|
|
|
+```java
|
|
|
+public interface Backupable {
|
|
|
+ // ===== 抽象方法(实现类必须实现) =====
|
|
|
+ void connect(); // 连接资源
|
|
|
+ void executeBackup(); // 执行备份
|
|
|
+ void close(); // 关闭资源
|
|
|
+
|
|
|
+ // ===== 默认方法(可选重写) =====
|
|
|
+ default boolean validateConfig(String path) {
|
|
|
+ if (path != null && !path.isEmpty()) {
|
|
|
+ System.out.println("配置校验通过:" + path);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ System.out.println("配置校验失败:路径不能为空");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ default void log(String operation, String status) {
|
|
|
+ System.out.println("[备份日志] " + operation + " —— " + status);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 模板方法(定义固定流程,使用 default 关键字) =====
|
|
|
+ default void backup(String configPath) {
|
|
|
+ // 1. 校验配置
|
|
|
+ if (!validateConfig(configPath)) return;
|
|
|
+
|
|
|
+ // 2. 连接资源
|
|
|
+ connect();
|
|
|
+
|
|
|
+ // 3. 执行备份
|
|
|
+ executeBackup();
|
|
|
+
|
|
|
+ // 4. 记录日志
|
|
|
+ log("数据备份", "完成");
|
|
|
+
|
|
|
+ // 5. 关闭资源
|
|
|
+ close();
|
|
|
+
|
|
|
+ System.out.println("--- 备份流程结束 ---");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== 静态方法 =====
|
|
|
+ static void printSystemInfo() {
|
|
|
+ System.out.println("数据备份系统 v2.0——支持数据库/文件/云备份");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 要求实现以下 3 个备份类
|
|
|
+
|
|
|
+**`DatabaseBackup` 类**(实现 Backupable):
|
|
|
+- **私有属性**:`dbName`(数据库名,String)
|
|
|
+- **构造方法**:`DatabaseBackup(String dbName)`
|
|
|
+- **实现** `connect()` —— 输出 `"连接数据库:xxx"`
|
|
|
+- **实现** `executeBackup()` —— 输出 `"执行数据库备份:导出表结构和数据..."`
|
|
|
+- **实现** `close()` —— 输出 `"关闭数据库连接"`
|
|
|
+- **重写** `log()` —— 输出 `"[数据库备份] xxx —— xxx"`(覆盖默认日志格式)
|
|
|
+
|
|
|
+**`FileBackup` 类**(实现 Backupable):
|
|
|
+- **私有属性**:`sourceDir`(源目录,String)
|
|
|
+- **构造方法**:`FileBackup(String sourceDir)`
|
|
|
+- **实现** `connect()` —— 输出 `"扫描本地目录:xxx"`
|
|
|
+- **实现** `executeBackup()` —— 输出 `"执行文件备份:压缩文件并复制到备份目录..."`
|
|
|
+- **实现** `close()` —— 输出 `"释放文件流资源"`
|
|
|
+- **注意**:不重写 `log()`,使用接口默认实现
|
|
|
+
|
|
|
+**`CloudBackup` 类**(实现 Backupable):
|
|
|
+- **私有属性**:`cloudProvider`(云服务商,String,如 "阿里云OSS" / "腾讯云COS")
|
|
|
+- **构造方法**:`CloudBackup(String cloudProvider)`
|
|
|
+- **实现** `connect()` —— 输出 `"连接云存储服务:xxx"`
|
|
|
+- **实现** `executeBackup()` —— 输出 `"执行云备份:上传数据到云端..."`(上传完成后追加输出 `"上传完成!"`)
|
|
|
+- **实现** `close()` —— 输出 `"断开云存储连接"`
|
|
|
+- **重写** `validateConfig()` —— 在父类校验基础上,额外检查 `cloudProvider` 是否以 "云" 结尾,如果不是则输出 `"云服务商名称不合法"` 并返回 false
|
|
|
+
|
|
|
+### 测试类要求
|
|
|
+
|
|
|
+**`BackupTest` 类**:
|
|
|
+
|
|
|
+1. 调用接口静态方法输出系统信息
|
|
|
+2. 创建三种备份对象,分别调用 `backup()` 方法
|
|
|
+3. 测试配置校验失败的情况(传入空路径)
|
|
|
+
|
|
|
+```java
|
|
|
+public class BackupTest {
|
|
|
+ public static void main(String[] args) {
|
|
|
+ // 1. 输出系统信息
|
|
|
+ Backupable.printSystemInfo();
|
|
|
+
|
|
|
+ // 2. 数据库备份
|
|
|
+ System.out.println("--- 数据库备份 ---");
|
|
|
+ Backupable dbBackup = new DatabaseBackup("ERP_System");
|
|
|
+ dbBackup.backup("/data/backup/db");
|
|
|
+
|
|
|
+ // 3. 文件备份
|
|
|
+ System.out.println("--- 文件备份 ---");
|
|
|
+ Backupable fileBackup = new FileBackup("/home/user/documents");
|
|
|
+ fileBackup.backup("/data/backup/files");
|
|
|
+
|
|
|
+ // 4. 云备份
|
|
|
+ System.out.println("--- 云备份 ---");
|
|
|
+ Backupable cloudBackup = new CloudBackup("阿里云OSS");
|
|
|
+ cloudBackup.backup("/data/backup/cloud");
|
|
|
+
|
|
|
+ // 5. 测试校验失败
|
|
|
+ System.out.println("--- 配置校验失败测试 ---");
|
|
|
+ dbBackup.backup("");
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 预期输出
|
|
|
+
|
|
|
+```
|
|
|
+数据备份系统 v2.0——支持数据库/文件/云备份
|
|
|
+--- 数据库备份 ---
|
|
|
+配置校验通过:/data/backup/db
|
|
|
+连接数据库:ERP_System
|
|
|
+执行数据库备份:导出表结构和数据...
|
|
|
+[数据库备份] 数据备份 —— 完成
|
|
|
+关闭数据库连接
|
|
|
+--- 备份流程结束 ---
|
|
|
+--- 文件备份 ---
|
|
|
+配置校验通过:/data/backup/files
|
|
|
+扫描本地目录:/home/user/documents
|
|
|
+执行文件备份:压缩文件并复制到备份目录...
|
|
|
+[备份日志] 数据备份 —— 完成
|
|
|
+释放文件流资源
|
|
|
+--- 备份流程结束 ---
|
|
|
+--- 云备份 ---
|
|
|
+配置校验通过:/data/backup/cloud
|
|
|
+连接云存储服务:阿里云OSS
|
|
|
+执行云备份:上传数据到云端...
|
|
|
+上传完成!
|
|
|
+[备份日志] 数据备份 —— 完成
|
|
|
+断开云存储连接
|
|
|
+--- 备份流程结束 ---
|
|
|
+--- 配置校验失败测试 ---
|
|
|
+配置校验失败:路径不能为空
|
|
|
+```
|
|
|
+
|
|
|
+### 对比思考
|
|
|
+
|
|
|
+| 对比维度 | 课堂 Notifiable + send() | 本作业 Backupable + backup() |
|
|
|
+|---------|------------------------|----------------------------|
|
|
|
+| 模板方法 | `send()` | `backup()` |
|
|
|
+| 抽象方法数量 | 1 个(`doSend`) | 3 个(`connect`、`executeBackup`、`close`) |
|
|
|
+| 默认方法数量 | 3 个(`validateParams`、`log`、`send`) | 3 个(`validateConfig`、`log`、`backup`) |
|
|
|
+| 静态方法 | `printNotificationType()` | `printSystemInfo()` |
|
|
|
+
|
|
|
+**思考题**:
|
|
|
+1. 模板方法 `backup()` 用 `default` 修饰而不是 `abstract`,为什么这样设计?
|
|
|
+2. 如果希望子类 **不能重写** 模板方法 `backup()`,应该用什么关键字修饰?(提示:回忆 abstract 中的 final 关键字)
|
|
|
+3. 观察 `DatabaseBackup` 重写了 `log()`,而 `FileBackup` 没有重写 —— 这体现了接口默认方法的什么优势?
|
|
|
+4. 本作业中 `Backupable` 是一个接口,如果改用抽象类实现,会有什么不同?(提示:单继承 vs 多实现)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 📋 题目总览
|
|
|
+
|
|
|
+| 序号 | 所属部分 | 核心知识点 | 难度 |
|
|
|
+|------|---------|-----------|------|
|
|
|
+| 练习 1 | 随堂 | 多态概念、三前提、父类引用指向子类对象 | ⭐ |
|
|
|
+| 练习 2 | 随堂 | 多态成员访问特点(编译看左运行看右) | ⭐⭐ |
|
|
|
+| 练习 3 | 随堂 | 向下转型、instanceof 运算符、调用子类特有方法 | ⭐⭐⭐ |
|
|
|
+| 练习 4 | 随堂 | 多态判断、多态与普通继承的区别 | ⭐⭐ |
|
|
|
+| 进阶 1 | 进阶 | 多态数组 + instanceof + 向下转型 + 员工管理综合 | ⭐⭐⭐⭐ |
|
|
|
+| 进阶 2 | 进阶 | 接口多态 + Usb 设备数组 + instanceof + 开闭原则 | ⭐⭐⭐⭐⭐ |
|
|
|
+| 作业 1 | 课后 | 接口默认方法/静态方法、通知系统扩展 | ⭐⭐⭐ |
|
|
|
+| 作业 2 | 课后 | 模板方法设计模式(接口默认方法实现)、数据备份系统 | ⭐⭐⭐⭐ |
|