2.2 一门多户:多态与动态绑定


2.2 一门多户:多态与动态绑定

本节摘要:多态的前提是继承加重写,形式是父类引用指向子类对象——引用类型决定"能调什么",实际对象决定"调谁的版本",这一步在运行时由动态绑定完成。向上转型自动且安全,向下转型需显式强转并可能抛类转换异常,instanceof 是强转前的安全检查。本节用两组实验拆开绑定过程,并给出多态在工程里的真正用法。

同一张叫号单,两个应答声

先看一段"反直觉"的代码。引用类型是 Staff,调用的却是 Intern 的版本:

class Staff { void duty() { System.out.println("普通员工:完成本职工作"); } } class Manager extends Staff { @Override void duty() { System.out.println("经理:统筹安排并签字审批"); } void sign() { System.out.println("经理:签批支出单"); } // 经理特有技能 } class Intern extends Staff { @Override void duty() { System.out.println("实习生:在导师指导下执行"); } void learnMentor() { System.out.println("实习生:参加导师辅导"); } // 实习生特有技能 } public class PolyDemo { public static void main(String[] args) { Staff s1 = new Manager(); // 向上转型:父类引用 指向 子类对象 Staff s2 = new Intern(); s1.duty(); // 输出:经理:统筹安排并签字审批 s2.duty(); // 输出:实习生:在导师指导下执行 } }

两行调用长得一模一样,引用.duty(),输出却不同。这就是多态:一个调用形式,多种实际表现。它的成立需要三个条件凑齐,缺一不可:有继承关系(Manager、Intern 与 Staff 同族);子类重写了父类方法(duty 被改版);父类引用指向子类对象(Staff s1 = new Manager())。第三条最容易被忽视——用 Manager m = new Manager() 直接调用,看到的只是"自己的方法",谈不上多态;多态必须是"隔着父类引用调用"。

拆开这句话的机制:编译期看左边,运行期看右边。编译器拿引用类型 Staff 检查"duty 这个方法存在吗、参数对吗"——这决定编译过不过;运行时 JVM 拿实际对象(Manager 的实例)去查它的方法表,从实际类型开始沿族谱向上找(2.1 节图 2-1 的查找路径),命中即执行。所以 s1.duty() 走的是 Manager 的版本。静态方法、final 方法、private 方法不参与这套动态查找——它们在编译期就绑死了,这也是"静态方法没有多态"这条结论的来源。

图 2-2 动态绑定:编译期定准入,运行期定版本

图 2-2 动态绑定:编译期定准入,运行期定版本

向下转型:危险动作与安全检查

向上转型(子类对象交给父类引用)自动、安全——因为它只是"把视野收窄",丢掉的只是子类特有成员的可见性,对象本身毫发无损。反方向的向下转型就要小心了:

public class CastDemo { public static void main(String[] args) { Staff s = new Intern(); // 向上转型 安全 // s.learnMentor(); // 编译报错:Staff 引用看不见 Intern 特有方法 Intern i = (Intern) s; // 向下转型:显式强转 i.learnMentor(); // 输出:实习生:参加导师辅导 Manager m = (Manager) s; // 编译能过 运行时抛 ClassCastException System.out.println("这行永远执行不到"); } }

第三行强转为什么炸?s 里装的实际是 Intern 的对象,硬说它是 Manager,运行时校验不通过,抛出 ClassCastException(差错登记科第 7 章的常客)。安全的做法是强转前用 instanceof 判断实际类型,JDK 14 以后还有"模式匹配"写法可以判断加强转一步到位:

public class SafeCast { public static void main(String[] args) { Staff[] roster = { new Manager(), new Intern() }; for (Staff s : roster) { s.duty(); // 多态调用:各应各的 if (s instanceof Manager) { // 经典写法:先判断后强转 ((Manager) s).sign(); // 输出:经理:签批支出单 } else if (s instanceof Intern it) { // 新写法:判断即绑定变量 it.learnMentor(); // 输出:实习生:参加导师辅导 } } } }

完整案例:值班表不认识具体的人

背景:调度系统每天生成值班表,值班主任、工程师、实习生各司其职。最初代码按具体类型分支:if 是主任就调主任的方法、else 调工程师的,每加一种角色就要改调度器。操作:把公共行为 onDuty 登记到父类 DutyMember,调度器只持有父类引用数组、只调 onDuty:

class DutyMember { void onDuty() { System.out.println("成员到岗,履行通用职责"); } } class DutyDirector extends DutyMember { @Override void onDuty() { System.out.println("主任:值守指挥席位,处置升级事件"); } } class DutyEngineer extends DutyMember { @Override void onDuty() { System.out.println("工程师:巡检系统,响应告警"); } } public class Roster { public static void main(String[] args) { DutyMember[] today = { new DutyDirector(), new DutyEngineer(), new DutyMember() }; for (DutyMember m : today) m.onDuty(); // 调度器只认 DutyMember // 输出三行:主任版本、工程师版本、通用版本 } }

结果:新增"值班司机"角色时,只需派一个子类重写 onDuty,调度器一行不改。解读:这就是多态的工程价值——调用方依赖稳定的父类接口,扩展发生在子类侧,新增不改旧,正是开闭原则的机制化。变式:如果角色间的差异不是"同一职责的不同做法",而是"有些角色根本没有这项职责",父类就不该提供通用实现,那属于第 3 章抽象类与接口的辖区;如果差异维度是"职责组合"而非"家族分支",接口比继承更合身。

本节要点回顾

  • 多态三条件:继承、重写、父类引用指向子类对象——第三条是"隔着引用调用"的形式前提
  • 编译期看左边、运行期看右边:引用类型管"能不能调",实际对象管"调谁的版本",后者由方法表查找完成(承接 2.1 节查找路径)
  • 静态、final、private 方法不参与动态绑定:它们编译期就绑死,"静态方法没有多态"由此而来
  • 向上转型安全、向下转型危险:强转前用 instanceof 检查实际类型,ClassCastException 是强转失败的标准报错
  • 多态的工程含义是依赖稳定签名:调用方只认父类,新增子类不改调用代码——开闭原则的机制基础

同一个 Staff 引用能应答不同版本,靠的是"所有对象都有方法表"这个底层事实。而所有类还共享一位共同始祖——下一节翻 Object 的档案:equals、hashCode、toString 三份契约,违一条,集合框架就会来收罚金。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U