封装、继承与多态
学习目标
- 用访问控制和不变式完成封装,理解
protected的可见性边界。 - 区分重载、重写、向上转型、向下转型和动态分派。
- 用父类引用调用子类实现,并判断组合是否比继承更合适。
核心知识点
专业术语
- 访问控制(access control):
private、包可见、protected、public控制成员可访问范围。 - 继承(inheritance):子类复用并扩展父类契约,表达一种可替换的
is-a关系。 - 方法覆盖(override):子类用相同签名提供实现,运行时实例方法可按真实类型动态分派。
- 向上转型(upcast):把子类对象视为父类类型,安全但只能直接使用父类声明的成员。
- 动态分派(dynamic dispatch):编译期确认可调用签名,运行时选择实际对象的重写方法。
白话解释与边界
封装隐藏表示细节,继承建立类型关系,多态让调用者依赖稳定抽象。变量的编译期类型决定“能写什么调用”,对象的运行时类型决定“实例方法执行哪份实现”;字段访问和静态方法不按对象动态分派。super 用于显式调用父类实现,不能把父类构造器当成可继承方法。
只有子类真正满足父类的行为契约时才适合继承;如果只是想复用几段代码,组合通常更安全。向下转型必须由真实运行时类型支持,先用 instanceof 或更好的多态设计避免 ClassCastException。protected 让子类接触内部状态,长期会扩大耦合面。
常用用法
private + 公开行为:封装可变状态
把字段设为 private,由方法集中校验状态;继承不是绕过父类不变式的理由。
// 语义:把字段设为 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());
// 输出:100extends:建立 is-a 类型关系
只有子类能够满足父类行为契约时才使用继承;单纯为了复用代码更适合组合。
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-1super:调用父类构造器或实现
super.method() 显式调用父类实现;super(...) 只能出现在构造器第一条语句,不能把父类构造器当成可继承方法。
// 关键变化: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 能在签名写错时让编译器报错,避免意外形成重载。
// 语义:@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向上转型:用父类引用接收子类对象
向上转型通常安全,但引用的编译期类型只暴露父类声明的成员;实例方法仍按运行时对象动态分派。
// 语义:向上转型通常安全,但引用的编译期类型只暴露父类声明的成员。
// 初始状态:report 初始为 new MarkdownReport()。
Report report = new MarkdownReport();
// 关键变化:report 接收表达式 new MarkdownReport() 的计算结果。
// 初始状态:report 的初始值为 new MarkdownReport()。
System.out.println(report.render());
// 输出:# report向下转型 + instanceof:安全访问子类能力
模式变量可在检查成功后使用;如果经常需要向下转型,通常说明父类或接口契约没有表达真正需要的行为。
// 语义:模式变量可在检查成功后使用。
// 初始状态: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 组织协作对象
组合把依赖作为字段注入,替换协作对象时不会把父类实现细节扩散到整个继承树。
// 语义:组合把依赖作为字段注入,替换协作对象时不会把父类实现细节扩散到整个继承树。
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 方法,而不是直接暴露可变字段。
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协变返回类型:重写时返回更具体类型
返回类型可以是原返回类型的子类型,但参数列表不能因此改变;参数改变属于重载,不是重写。
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字段与静态方法:不会按实例动态分派
实例方法才支持动态分派;字段按声明类型解析,静态方法按引用的编译期类型解析,不能当成多态行为使用。
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继续阅读
- 构造器与初始化顺序:父子类构造调用和初始化阶段。
- 接口与抽象类:用接口表达可替换能力,用抽象类共享骨架。
static、final与代码组织:静态成员、常量和不可变扩展边界。
简单案例
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;仅为共享代码而继承会把父类变化传播给子类。
课后小问
Report report = new MarkdownReport()时,为什么调用report.render()会输出 Markdown? 答案:render是实例方法,运行时根据对象的真实类型MarkdownReport进行动态分派。 解析:变量的静态类型只决定调用是否合法;只要签名可见,JVM 会选择最具体的覆盖实现。向下转型前检查
instanceof能解决什么问题? 答案:它能在转型前确认对象确实是目标类型或其子类型,避免不匹配造成的ClassCastException。 解析:检查只保护类型转换这一刻,不能替代清晰的多态契约;若经常需要向下转型,通常说明抽象设计还可改进。
本节小结
- 封装通过访问控制和行为方法维护不变式,继承必须满足可替换契约。
- 编译期类型决定可见调用,运行时类型决定实例方法的动态分派。
- 重载、重写和转型解决不同问题,
@Override能帮助编译器检查签名。 - 复用代码时优先考虑组合,避免 protected 状态和错误继承造成耦合。
快速回顾
- 能用
is-a与has-a判断继承或组合。 - 能解释父类引用调用子类方法的动态分派过程。
- 能区分重载、重写、向上转型和向下转型。
- 能指出字段、静态方法与实例方法的绑定差异。
阿源的知识库