8.2 多态与动态分派
前置知识:继承、方法签名和编译期类型
预计时间:30 分钟
完成标志:能预测实例方法分派、识别不参与多态的成员,并把向下转型限制在明确边界
上一页的 EmailSender 用自己的 send() 替换了继承来的实现,这就叫方法重写;@Override 请编译器检查是否确实存在可重写的方法。具体签名与权限规则下一页再讲。现在先观察调用发生时会选谁。
前四节沿用上一页的 MessageSender 和双参数 EmailSender,再加入一个演示短信通道和调度中心。所有通道都只打印信息:
class SmsSender extends MessageSender {
SmsSender(String message) { super(message); }
@Override public void send() {
logMessage();
System.out.println("demo SMS channel");
}
}
final class DispatchCenter {
void dispatch(MessageSender sender) {
java.util.Objects.requireNonNull(sender, "sender");
sender.send();
}
}1. 编译期决定可调用成员,运行时选择覆盖实现
MessageSender email = new EmailSender("容量告警", "ops@example.com");
MessageSender sms = new SmsSender("服务已恢复");
DispatchCenter center = new DispatchCenter();
center.dispatch(email);
center.dispatch(sms);变量的编译期类型是 MessageSender,所以源码只能直接调用这个类型可见的成员。执行 sender.send() 时,JVM 根据接收者实际类选择 EmailSender.send 或 SmsSender.send。这叫实例方法的动态分派。
它不是编译器在运行时替你生成一串 if (sender instanceof ...)。JVM 可使用虚方法表、内联缓存和去虚拟化等实现策略,但这些不改变语言层的分派契约。
若接收者为 null,没有实际类可供分派,调用会抛 NullPointerException。
2. 多态的价值来自稳定契约
加入 BeaconSender 时,调度中心无需修改,前提是新类型遵守 MessageSender.send 的全部约定。若子类对合法消息任意抛错、改变成功含义或破坏可观察顺序,“能编译”也不代表可安全替换。
因此扩展性不是“加任何子类都一行不改”的绝对承诺。调用方只在依赖稳定抽象、且新实现满足契约时保持关闭。配置、依赖注入和错误处理仍可能需要更新。
3. static 方法和字段不参与这种动态分派
class Parent {
String field = "parent";
static String kind() { return "parent"; }
String instanceKind() { return "parent"; }
}
class Child extends Parent {
String field = "child";
static String kind() { return "child"; }
@Override String instanceKind() { return "child"; }
}
Parent value = new Child();
System.out.println(value.field); // parent
System.out.println(Parent.kind()); // parent
System.out.println(value.instanceKind()); // child只有最后一个普通实例方法发生覆盖分派。字段按表达式编译期类型解析,static 方法应用类名表达归属。
4. 向上转型安全,向下转型需要证明
子类对象赋给父类变量是向上转型,不需要强制语法:
MessageSender sender = new EmailSender("消息", "ops@example.com");反方向不能仅凭变量类型保证:
// EmailSender email = (EmailSender) sender;若实际对象不是 EmailSender,强制转换抛 ClassCastException。Java 17 可用模式匹配同时检查和绑定:
if (sender instanceof EmailSender email) {
System.out.println(email.recipient());
}null instanceof EmailSender 返回 false。模式变量只在编译器能证明匹配成功的作用域内可用。
instanceof 适合解析外部边界、兼容旧 API 或确实需要子类专有信息的场景。若调度中心到处按具体类型分支,每新增一种发送器都要修改分支,说明缺少合适的抽象操作或访问者等设计。
5. 接口通常比具体父类更适合调用方
当前用 MessageSender 便于展示类继承。真正的调度中心只需要“能发送”的能力,可以依赖更小契约:
interface Sendable {
void send();
}
final class DispatchCenter {
void dispatch(Sendable sender) {
java.util.Objects.requireNonNull(sender, "sender").send();
}
}EmailChannel、SmsChannel 和 ConsoleChannel 可共享抽象基类,也可通过组合获得公共格式化器;只要实现 Sendable,调度中心不关心其继承树。方法重写一讲之后,我们会比较接口与抽象类。
6. 一个可运行的最小分派
以下是独立版本,保存为 DispatchDemo.java 后编译运行。这里把 send() 改成返回演示文本;不要与前面返回 void 的同名接口放进一个源文件。它仍不执行真实发送。
interface Sendable {
String send();
}
final class EmailChannel implements Sendable {
@Override
public String send() {
return "demo email channel";
}
}
final class SmsChannel implements Sendable {
@Override
public String send() {
return "demo SMS channel";
}
}
public class DispatchDemo {
static void dispatch(Sendable sender) {
System.out.println(sender.send());
}
public static void main(String[] args) {
dispatch(new EmailChannel());
dispatch(new SmsChannel());
}
}输出:
demo email channel
demo SMS channel专业速查
| 情况 | 选择 |
|---|---|
| 只需要共同能力 | 依赖小接口 |
| 需要共享稳定状态/骨架 | 受控抽象基类 |
| 只复用实现 | 优先组合/委托 |
| 必须读取子类专有信息 | 边界处模式匹配 |
| 大量具体类型分支 | 重新检查抽象契约 |
离开这一页前
让同一个 DispatchCenter 依次接收 EmailChannel、SmsChannel 和 ConsoleChannel,标出编译期允许调用哪些成员、运行期选择哪个覆盖实现。字段和 static 方法不参与这种分派;错误向下转换要看到 ClassCastException,null instanceof EmailSender 则应为 false。若核心流程不断按具体类型分支,回头检查抽象契约。
规范入口:JLS §15.12.4.4:运行时选择目标方法。