说明:以下习题围绕今日授课核心内容——多态(Polymorphism) 展开,涵盖多态的概念与前提、多态成员访问特点、向上转型/向下转型、instanceof 运算符、多态与抽象类/接口的结合、接口默认方法/静态方法以及模板方法设计模式。
题目分为三部分:随堂练习(多态基础,4 题)、进阶练习(综合挑战,2 题)和 课后作业(接口默认方法/静态方法 + 模板方法,2 题)。
以下练习围绕多态的基础概念和用法,由浅入深。
难度:⭐
知识点:多态概念、多态三个前提条件、父类引用指向子类对象
创建一个宠物喂食体系,体验 同一个方法调用,不同子类表现出不同的行为。
Pet 类(父类):
void eat() —— 输出 "宠物在吃饭..."Dog 类(继承 Pet):
eat() —— 输出 "小狗在啃骨头..."Cat 类(继承 Pet):
eat() —— 输出 "小猫在吃鱼..."测试类 PetTest 要求:
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() 有什么缺点?
}
}
预期输出:
小狗在啃骨头...
小猫在吃鱼...
思考题:
eat() 方法没有被重写,输出会是什么?为什么?Pet 类中还有 sleep() 方法但 Dog 和 Cat 没有重写它,通过 p1.sleep() 调用会执行哪个类的 sleep()?难度:⭐⭐
知识点:多态中成员变量和成员方法的访问特点、编译期 vs 运行期
通过代码验证多态的成员访问规则。
交通工具 体系:
Vehicle 类(父类):
String name = "交通工具"void run() —— 输出 "交通工具在行驶..."void stop() —— 输出 "交通工具刹车停止"static void info() —— 输出 "Vehicle 静态方法"Car 类(继承 Vehicle):
String name = "小汽车"run() —— 输出 "小汽车在高速行驶..."void openSunroof() —— 输出 "打开天窗~"测试类要求:
编写 VehicleTest 类,创建一个 Car 对象,分别使用「父类引用指向子类对象」和「子类引用指向子类对象」两种方式,观察成员变量、成员方法、特有方法的访问差异。
模板代码:
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() → __________
总结填空:
难度:⭐⭐⭐
知识点:向上转型、向下转型、instanceof 运算符、子类特有方法的调用
场景描述:动物园里有多种动物,管理员需要根据不同的动物执行不同的操作。有些动物会游泳,有些动物会飞翔,这些是特有行为。
Animal 类(父类):
void eat() —— 输出 "动物在吃东西"void sleep() —— 输出 "动物在睡觉"Fish 类(继承 Animal):
eat() —— 输出 "鱼儿在吃饲料"sleep() —— 输出 "鱼儿睁着眼睛睡觉"void swim() —— 输出 "鱼儿在水中游来游去~"Bird 类(继承 Animal):
eat() —— 输出 "小鸟在啄米"sleep() —— 输出 "小鸟在巢穴里睡觉"void fly() —— 输出 "小鸟在天空中飞翔~"请完成 ZooKeeper 类:
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);
}
}
预期输出:
鱼儿在吃饲料
鱼儿睁着眼睛睡觉
鱼儿在水中游来游去~
小鸟在啄米
小鸟在巢穴里睡觉
小鸟在天空中飞翔~
思考题:
keeper.handleAnimal(new Animal()) 会发生什么?instanceof 判断会进入哪个分支?instanceof 判断,直接强制转型 (Fish) animal,当传入 Bird 对象时会怎样?animal.swim() 可以吗?为什么?难度:⭐⭐
知识点:多态的判断、多态与普通继承的区别
判断以下代码片段中,哪些体现了多态,哪些没有,并说明理由。
| 序号 | 代码片段 | 是否多态? | 理由 |
|---|---|---|---|
| 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"; |
拓展思考:
// 以下代码是否是多态?为什么?
public void feed(Pet pet) {
pet.eat();
}
// 调用
feed(new Dog());
feed(new Cat());
参考答案思路:
| 序号 | 是否多态? | 理由 |
|---|---|---|
| 1 | ✅ 是 | 父类引用指向子类对象,调用重写方法 —— 满足多态三前提 |
| 2 | ❌ 否 | 子类引用指向子类对象,没有「父类引用指向子类对象」 |
| 3 | ✅ 是 | 接口引用指向实现类对象,属于接口多态 |
| 4 | ❌ 否 | 没有父类引用,没有方法重写的调用 |
| 5 | ✅ 是 | 先向上转型(多态),再向下转型恢复子类完整形态 |
| 6 | ✅ 是 | String 是 Object 的子类,属于继承多态 |
以下为综合挑战题,融合多态、向上转型/向下转型、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 * hoursWorkedvoid clockIn() —— 输出 "兼职员工 xxx 打卡上班..."(xxx 为姓名)Intern 类(继承 Employee):
allowance(实习津贴,double)Intern(int id, String name, double allowance)calculateSalary() —— 返回 allowance(固定津贴,无其他扣减)void learnSkill() —— 输出 "实习生 xxx 在学习新技能..."(xxx 为姓名)EmployeeManager 类:
Employee[] 数组,包含至少 5 名员工(各类型混合)| 类型 | id | 姓名 | 参数 | |------|----|------|------| | FullTimeEmployee | 1001 | 张三 | 基本工资 8000,绩效 2000 | | PartTimeEmployee | 2001 | 李四 | 时薪 50,工作 80 小时 | | Intern | 3001 | 王五 | 津贴 2000 | | FullTimeEmployee | 1002 | 赵六 | 基本工资 10000,绩效 3000 | | PartTimeEmployee | 2002 | 孙七 | 时薪 45,工作 60 小时 |
遍历数组输出基本信息:调用每个元素的 showInfo() 方法
遍历数组,按类型执行特有行为:
FullTimeEmployee → 向下转型 → 调用 attendMeeting()PartTimeEmployee → 向下转型 → 调用 clockIn()Intern → 向下转型 → 调用 learnSkill()计算总薪资成本:遍历数组,累加所有员工的 calculateSalary(),输出总薪资
查找薪资最高的员工:遍历数组,找出 calculateSalary() 最大的员工,输出其信息
Employee[] 数组中存储的是不同类型对象,这体现了面向对象哪个特征?instanceof 判断类型,直接向下转型会有什么风险?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)
基于课堂上的 Usb 接口案例,扩展实现一个 Usb 设备管理器。要求:
NoteBook 类可以接入多个 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 数组,对每个设备:
startUsb()instanceof 判断设备类型,向下转型调用特有方法
Mouse → 调用 click()Keyboard → 调用 type()USBHub → 调用 expandPorts()stopUsb()UsbManagerTest 类:
NoteBook 对象("ThinkPad")Mouse("罗技")Keyboard("QWERTY")USBHub(4)Mouse("雷蛇")workAll() 让所有设备工作一轮========== ThinkPad 笔记本 Usb 设备工作 ==========
鼠标已连接(品牌:罗技)
鼠标单击——嗒
鼠标已断开
键盘已连接(布局:QWERTY)
键盘输入:Hello World!
键盘已断开
USB 扩展坞已连接(4 个接口)
扩展出 4 个 USB 接口,可连接更多设备
USB 扩展坞已断开
鼠标已连接(品牌:雷蛇)
鼠标单击——嗒
鼠标已断开
NoteBook 类中的 workAll() 方法,参数和返回值的类型设计体现了什么原则?Printer(打印机)Usb 设备,需要修改 NoteBook 类的代码吗?这符合哪个设计原则?Test02.java,本练习增加了什么功能?(提示:从单个设备到多个设备 + instanceof 判断)请补全 NoteBook 类的 workAll() 方法:
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 新特性(默认方法、静态方法)和模板方法设计模式,结合课堂通知系统案例进行扩展。
难度:⭐⭐⭐
知识点:接口默认方法(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() —— 输出系统信息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 类:
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]消息:发送成功
--- 发送流程结束 ---
--- 空参数测试 ---
参数校验失败:收件人或消息不能为空
SMSNotifier 没有重写 log() 方法,调用时执行的是谁的 log()?printNotificationType() 能通过实现类对象调用吗?如 sms.printNotificationType() —— 尝试后说说你的发现。Notifiable 新增一个默认方法 default void sendWithRetry(...),已有的 3 个实现类需要修改吗?这体现了接口默认方法的什么优势?难度:⭐⭐⭐⭐
知识点:模板方法设计模式、接口默认方法、抽象方法与默认方法的配合
开发一个数据备份系统,所有备份任务都遵循相同的流程,但不同的备份方式(数据库备份、文件备份、云备份)在具体执行步骤上有所不同。
核心流程(模板方法定义):
Backupable 接口:
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——支持数据库/文件/云备份");
}
}
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 是否以 "云" 结尾,如果不是则输出 "云服务商名称不合法" 并返回 falseBackupTest 类:
backup() 方法测试配置校验失败的情况(传入空路径)
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() |
思考题:
backup() 用 default 修饰而不是 abstract,为什么这样设计?backup(),应该用什么关键字修饰?(提示:回忆 abstract 中的 final 关键字)DatabaseBackup 重写了 log(),而 FileBackup 没有重写 —— 这体现了接口默认方法的什么优势?Backupable 是一个接口,如果改用抽象类实现,会有什么不同?(提示:单继承 vs 多实现)| 序号 | 所属部分 | 核心知识点 | 难度 |
|---|---|---|---|
| 练习 1 | 随堂 | 多态概念、三前提、父类引用指向子类对象 | ⭐ |
| 练习 2 | 随堂 | 多态成员访问特点(编译看左运行看右) | ⭐⭐ |
| 练习 3 | 随堂 | 向下转型、instanceof 运算符、调用子类特有方法 | ⭐⭐⭐ |
| 练习 4 | 随堂 | 多态判断、多态与普通继承的区别 | ⭐⭐ |
| 进阶 1 | 进阶 | 多态数组 + instanceof + 向下转型 + 员工管理综合 | ⭐⭐⭐⭐ |
| 进阶 2 | 进阶 | 接口多态 + Usb 设备数组 + instanceof + 开闭原则 | ⭐⭐⭐⭐⭐ |
| 作业 1 | 课后 | 接口默认方法/静态方法、通知系统扩展 | ⭐⭐⭐ |
| 作业 2 | 课后 | 模板方法设计模式(接口默认方法实现)、数据备份系统 | ⭐⭐⭐⭐ |