跳到内容

第 1 卷 / 第 8 章 / 第 2 课

8.2 多态与动态分派 ​

前置知识:继承、方法签名和编译期类型

预计时间:30 分钟

完成标志:能预测实例方法分派、识别不参与多态的成员,并把向下转型限制在明确边界

上一页的 EmailSender 用自己的 send() 替换了继承来的实现,这就叫方法重写;@Override 请编译器检查是否确实存在可重写的方法。具体签名与权限规则下一页再讲。现在先观察调用发生时会选谁。

前四节沿用上一页的 MessageSender 和双参数 EmailSender,再加入一个演示短信通道和调度中心。所有通道都只打印信息:

java
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. 编译期决定可调用成员,运行时选择覆盖实现 ​

java
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 方法和字段不参与这种动态分派 ​

java
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. 向上转型安全,向下转型需要证明 ​

子类对象赋给父类变量是向上转型,不需要强制语法:

java
MessageSender sender = new EmailSender("消息", "ops@example.com");

反方向不能仅凭变量类型保证:

java
// EmailSender email = (EmailSender) sender;

若实际对象不是 EmailSender,强制转换抛 ClassCastException。Java 17 可用模式匹配同时检查和绑定:

java
if (sender instanceof EmailSender email) {
    System.out.println(email.recipient());
}

null instanceof EmailSender 返回 false。模式变量只在编译器能证明匹配成功的作用域内可用。

instanceof 适合解析外部边界、兼容旧 API 或确实需要子类专有信息的场景。若调度中心到处按具体类型分支,每新增一种发送器都要修改分支,说明缺少合适的抽象操作或访问者等设计。

5. 接口通常比具体父类更适合调用方 ​

当前用 MessageSender 便于展示类继承。真正的调度中心只需要“能发送”的能力,可以依赖更小契约:

java
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 的同名接口放进一个源文件。它仍不执行真实发送。

java
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());
    }
}

输出:

text
demo email channel
demo SMS channel

专业速查 ​

情况选择
只需要共同能力依赖小接口
需要共享稳定状态/骨架受控抽象基类
只复用实现优先组合/委托
必须读取子类专有信息边界处模式匹配
大量具体类型分支重新检查抽象契约

离开这一页前 ​

让同一个 DispatchCenter 依次接收 EmailChannel、SmsChannel 和 ConsoleChannel,标出编译期允许调用哪些成员、运行期选择哪个覆盖实现。字段和 static 方法不参与这种分派;错误向下转换要看到 ClassCastException,null instanceof EmailSender 则应为 false。若核心流程不断按具体类型分支,回头检查抽象契约。

→ 下一步:方法重写、@Override 与 super

规范入口:JLS §15.12.4.4:运行时选择目标方法。

Built with VitePress | Software Systems Atlas