Skip to content

封装、继承与多态 ​

学习目标 ​

  • 用访问控制和不变式完成封装,理解 protected 的可见性边界。
  • 区分重载、重写、向上转型、向下转型和动态分派。
  • 用父类引用调用子类实现,并判断组合是否比继承更合适。

核心知识点 ​

专业术语 ​

  • 访问控制(access control):private、包可见、protected、public 控制成员可访问范围。
  • 继承(inheritance):子类复用并扩展父类契约,表达一种可替换的 is-a 关系。
  • 方法覆盖(override):子类用相同签名提供实现,运行时实例方法可按真实类型动态分派。
  • 向上转型(upcast):把子类对象视为父类类型,安全但只能直接使用父类声明的成员。
  • 动态分派(dynamic dispatch):编译期确认可调用签名,运行时选择实际对象的重写方法。

白话解释与边界 ​

封装隐藏表示细节,继承建立类型关系,多态让调用者依赖稳定抽象。变量的编译期类型决定“能写什么调用”,对象的运行时类型决定“实例方法执行哪份实现”;字段访问和静态方法不按对象动态分派。super 用于显式调用父类实现,不能把父类构造器当成可继承方法。

只有子类真正满足父类的行为契约时才适合继承;如果只是想复用几段代码,组合通常更安全。向下转型必须由真实运行时类型支持,先用 instanceof 或更好的多态设计避免 ClassCastException。protected 让子类接触内部状态,长期会扩大耦合面。

常用用法 ​

private + 公开行为:封装可变状态 ​

把字段设为 private,由方法集中校验状态;继承不是绕过父类不变式的理由。

java
// 语义:把字段设为 private,由方法集中校验状态。
// 初始状态:wallet 初始为 new Wallet()。
class Wallet {
    private int cents;

    void add(int amount) {
    // 作用:把字段设为 private,由方法集中校验状态;继承不是绕过父类不变式的理由。
        if (amount <= 0) throw new IllegalArgumentException("amount");
// 异常:传入 amount<=0 时抛出 message="amount" 的 IllegalArgumentException。
        cents += amount;
    }

    int cents() {
        return cents;
    }
}

Wallet wallet = new Wallet();
// 初始状态:wallet.cents 为 0。
// 关键变化:new Wallet() 创建 wallet,private 字段 cents 从默认值 0 开始。
wallet.add(100);
// 关键变化:wallet.add(100) 校验正数后把 cents 从 0 增加到 100。
System.out.println(wallet.cents());
// 输出:100

extends:建立 is-a 类型关系 ​

只有子类能够满足父类行为契约时才使用继承;单纯为了复用代码更适合组合。

java
class Document {
// 作用:只有子类能够满足父类行为契约时才使用继承;单纯为了复用代码更适合组合。
    String kind() {
        return "document";
    }
}

class Invoice extends Document {
// 作用:只有子类能够满足父类行为契约时才使用继承。
    String number() {
        return "INV-1";
    }
}

Invoice invoice = new Invoice();
// 关键变化:invoice 接收表达式 new Invoice() 的计算结果。
// 初始状态:invoice 当前为 new Invoice()。
System.out.println(invoice.kind() + ", " + invoice.number());
// 输出:document, INV-1

super:调用父类构造器或实现 ​

super.method() 显式调用父类实现;super(...) 只能出现在构造器第一条语句,不能把父类构造器当成可继承方法。

java
// 关键变化:Child.label 先用 super.label() 取得父类的 "base",再追加 "+child"。
class Base {
// 作用:super.method() 显式调用父类实现;super(...) 只能出现在构造器第一条语句,不能把父类构造器当成可继承方法。
    String label() {
        return "base";
    }
}

class Child extends Base {
    @Override
    String label() {
        return super.label() + "+child";
    }
}

System.out.println(new Child().label());
// 输出:base+child

@Override:让编译器检查重写 ​

@Override 能在签名写错时让编译器报错,避免意外形成重载。

java
// 语义:@Override 能在签名写错时让编译器报错,避免意外形成重载。
class Report {
    String render() {
        return "report";
    }
}

class MarkdownReport extends Report {
    @Override
    String render() {
// 说明:@Override 标记下面的 render(),编译器会检查它准确重写 Report.render()。
    // 作用:@Override 能在签名写错时让编译器报错,避免意外形成重载。
        return "# report";
    }
}

System.out.println(new MarkdownReport().render());
// 输出:# report

向上转型:用父类引用接收子类对象 ​

向上转型通常安全,但引用的编译期类型只暴露父类声明的成员;实例方法仍按运行时对象动态分派。

java
// 语义:向上转型通常安全,但引用的编译期类型只暴露父类声明的成员。
// 初始状态:report 初始为 new MarkdownReport()。
Report report = new MarkdownReport();
// 关键变化:report 接收表达式 new MarkdownReport() 的计算结果。
// 初始状态:report 的初始值为 new MarkdownReport()。
System.out.println(report.render());
// 输出:# report

向下转型 + instanceof:安全访问子类能力 ​

模式变量可在检查成功后使用;如果经常需要向下转型,通常说明父类或接口契约没有表达真正需要的行为。

java
// 语义:模式变量可在检查成功后使用。
// 初始状态:report 初始为 new MarkdownReport()。
Report report = new MarkdownReport();
// 关键变化:report 接收表达式 new MarkdownReport() 的计算结果。
// 初始状态:report 的初始值为 new MarkdownReport()。
if (report instanceof MarkdownReport markdown) {
    System.out.println(markdown.render());
    // 输出:# report
}

组合:用 has-a 组织协作对象 ​

组合把依赖作为字段注入,替换协作对象时不会把父类实现细节扩散到整个继承树。

java
// 语义:组合把依赖作为字段注入,替换协作对象时不会把父类实现细节扩散到整个继承树。
class Printer {
    String print(String text) {
        return "print:" + text;
    }
}

class ReportService {
    private final Printer printer;

    ReportService(Printer printer) {
        this.printer = printer;
    }

    String send(String text) {
        return printer.print(text);
    }
}

System.out.println(new ReportService(new Printer()).send("ok"));
// 输出:print:ok

不常用但需要知道 ​

protected:给子类开放有限访问 ​

protected 还涉及同包访问和跨包子类访问规则,容易扩大耦合;优先给子类提供受控的 protected 方法,而不是直接暴露可变字段。

java
class BaseCounter {
// 作用:protected 还涉及同包访问和跨包子类访问规则,容易扩大耦合;优先给子类提供受控的 protected 方法,而不是直接暴露可变字段。
// 初始状态:BaseCounter.value 默认为 0,ChildCounter.increment 可通过 protected 访问将它增加到 1。
    protected int value;
}

class ChildCounter extends BaseCounter {
    void increment() {
        value++;
    }
}

ChildCounter counter = new ChildCounter();
// 关键变化:counter 接收表达式 new ChildCounter() 的计算结果。
// 初始状态:counter 的初始值为 new ChildCounter()。
counter.increment();
// 关键变化:counter.increment() 让 protected value 从 0 增加到 1。
System.out.println(counter.value);
// 输出:1

协变返回类型:重写时返回更具体类型 ​

返回类型可以是原返回类型的子类型,但参数列表不能因此改变;参数改变属于重载,不是重写。

java
class BaseCopy {
    BaseCopy copy() {
        return new BaseCopy();
// 返回:new BaseCopy() 创建一个新的 BaseCopy 实例;本例没有携带额外字段状态。
    }
}

class ChildCopy extends BaseCopy {
    @Override
    ChildCopy copy() {
        return new ChildCopy();
    }
}

System.out.println(new ChildCopy().copy().getClass().getSimpleName());
// 输出:ChildCopy

字段与静态方法:不会按实例动态分派 ​

实例方法才支持动态分派;字段按声明类型解析,静态方法按引用的编译期类型解析,不能当成多态行为使用。

java
class Parent {
    String name = "parent";
// 关键变化:name 接收表达式 "parent" 的计算结果。
// 初始状态:name 的初始值为 "parent"。
    static String kind() { return "parent"; }
}

class Child extends Parent {
    String name = "child";
    static String kind() { return "child"; }
}

Parent value = new Child();
System.out.println(value.name + ", " + value.kind());
// 输出:parent, parent

继续阅读 ​

简单案例 ​

java
class Report {
    String render() {
        return "generic report";
    }
}

final class MarkdownReport extends Report {
    @Override
    String render() {
        return "# markdown report";
    }
}

public class PolymorphismDemo {
    static String print(Report report) {
        return report.render(); // 动态分派到真实对象
    }

    public static void main(String[] args) {
        Report report = new MarkdownReport(); // 向上转型
        System.out.println(print(report));
        // 输出:# markdown report
        System.out.println("runtime=" + report.getClass().getSimpleName());
        // 输出:runtime=MarkdownReport
    }
}

输出为 # markdown report 和 runtime=MarkdownReport。print 的参数编译期类型是 Report,因此不能直接调用子类私有 API;但 render() 是实例方法,运行时会执行 MarkdownReport 的覆盖实现。

易混点 ​

  • 重载按编译期参数选择,重写按运行时对象选择;修改参数列表会形成重载而不是重写。
  • 向上转型通常安全,向下转型只是改变看法,真实对象不匹配时仍会抛 ClassCastException。
  • 实例方法支持动态分派,字段和 static 方法按声明类型解析,不能把它们混为一谈。
  • 继承表达 is-a,组合表达 has-a;仅为共享代码而继承会把父类变化传播给子类。

课后小问 ​

  1. Report report = new MarkdownReport() 时,为什么调用 report.render() 会输出 Markdown? 答案:render 是实例方法,运行时根据对象的真实类型 MarkdownReport 进行动态分派。 解析:变量的静态类型只决定调用是否合法;只要签名可见,JVM 会选择最具体的覆盖实现。

  2. 向下转型前检查 instanceof 能解决什么问题? 答案:它能在转型前确认对象确实是目标类型或其子类型,避免不匹配造成的 ClassCastException。 解析:检查只保护类型转换这一刻,不能替代清晰的多态契约;若经常需要向下转型,通常说明抽象设计还可改进。

本节小结 ​

  • 封装通过访问控制和行为方法维护不变式,继承必须满足可替换契约。
  • 编译期类型决定可见调用,运行时类型决定实例方法的动态分派。
  • 重载、重写和转型解决不同问题,@Override 能帮助编译器检查签名。
  • 复用代码时优先考虑组合,避免 protected 状态和错误继承造成耦合。

快速回顾 ​

  • 能用 is-a 与 has-a 判断继承或组合。
  • 能解释父类引用调用子类方法的动态分派过程。
  • 能区分重载、重写、向上转型和向下转型。
  • 能指出字段、静态方法与实例方法的绑定差异。