本节摘要:多态的前提是继承加重写,形式是父类引用指向子类对象——引用类型决定"能调什么",实际对象决定"调谁的版本",这一步在运行时由动态绑定完成。向上转型自动且安全,向下转型需显式强转并可能抛类转换异常,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 方法不参与这套动态查找——它们在编译期就绑死了,这也是"静态方法没有多态"这条结论的来源。

向上转型(子类对象交给父类引用)自动、安全——因为它只是"把视野收窄",丢掉的只是子类特有成员的可见性,对象本身毫发无损。反方向的向下转型就要小心了:
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 章抽象类与接口的辖区;如果差异维度是"职责组合"而非"家族分支",接口比继承更合身。
同一个 Staff 引用能应答不同版本,靠的是"所有对象都有方法表"这个底层事实。而所有类还共享一位共同始祖——下一节翻 Object 的档案:equals、hashCode、toString 三份契约,违一条,集合框架就会来收罚金。