面向对象(下):继承、super、final、抽象、接口、多态、Object 与内部类
当两个类有相似行为时,是继承一个父类,还是把公共功能交给另一个对象?这一章不只介绍语法,也讨论这种选择。先看重写和多态怎样影响实际调用,再判断抽象类、接口和组合各适合什么场景。
课件来源:《第4章 面向对象(下).pptx》。本文逐项覆盖课件目录,并依据 Java SE 26、JDK 25 LTS 及相关官方文档补充现代工程实践。
本章覆盖课件中的继承与重写、super、final、抽象类、接口、多态、类型转换、Object 和四类内部类,并从组合优先和契约设计角度校准。
1. 学习目标
- 理解继承的 is-a 约束与组合的 has-a 关系。
- 掌握重写、动态分派、向上/向下转型和模式匹配。
- 正确实现 equals/hashCode/toString。
- 选择抽象类、接口、sealed hierarchy 和内部类。
2. 知识结构
3. 逐项详解
1. 继承与 is-a 关系
extends 建立子类型关系,子类继承可访问成员并可增加行为。Java 类只支持单继承,但接口可多实现。继承必须满足里氏替换:使用父类型的代码不应因替换为子类而破坏契约。
工程理解: 共享实现并不是采用继承的充分理由;优先组合与委托,把变化点放在小接口后。
常见误区: 为了复用几行代码建立脆弱继承树,或子类加强前置条件、削弱后置条件。
2. 方法重写与动态分派
重写要求实例方法签名兼容,返回类型可协变,访问级别不可更严格,受检异常不可更宽。运行时根据实际对象类型选择重写方法;字段和 static 方法不参与这种动态分派。
工程理解: 始终使用 @Override 让编译器检查;父类契约应写清副作用和异常。
常见误区: 把重载与重写混淆,或以为 static 方法会多态。
3. super 与父类构造
super.member 用于访问父类中可访问的成员,例如调用被重写的父类方法。构造器规则要按 Java 版本理解:“显式 super(...) 或 this(...) 必须放在第一条语句”是旧教程的规则,Java 25 已正式支持灵活构造器体。
现在可以先检查参数、计算局部变量,再调用父类构造器。好处是无效参数可以在进入父类构造逻辑前被拒绝。不过这段前置代码处于“早期构造上下文”,不是一个已经可自由使用的对象:实例方法调用、实例字段读取等仍受限制;允许的本类字段赋值也要遵循具体规则。日常代码先用参数校验和局部计算这两种简单用途,不要把任意业务逻辑搬到 super(...) 前面。
例如,Child(int n) 可先执行 if (n < 0) throw new IllegalArgumentException();,再执行 super(n);。父类构造器返回后,才继续子类的实例初始化器及构造器后续部分;不要再概括成“所有子类代码都在父类构造后运行”。版本和限制见 Java 25 灵活构造器体。
工程理解: 父类构造期间避免调用可重写方法,否则子类字段尚未初始化。
常见误区: 认为子类继承了父类构造器,或在构造器中访问未完成初始化的多态状态。
4. final 的三种语义
final 类不可继承,final 实例方法不可重写,final 变量只能赋值一次。final 引用仍可能指向可变对象;真正不可变还要求状态私有、无可变泄漏和线程安全发布。
工程理解: 值对象和不可变配置优先 final 字段;设计扩展点时只开放有稳定契约的方法。
常见误区: 把 final 引用与深度不可变对象等同。
5. 抽象类
抽象类可包含状态、构造器、具体方法和抽象方法,适合同一族对象共享不变量与部分实现。抽象类不能直接实例化,但其构造器会在子类构造过程中执行。
工程理解: 模板方法适用于骨架稳定、个别步骤变化的算法;扩展点要小而明确。
常见误区: 用巨型抽象基类承载所有公共代码,迫使无关子类继承无用能力。
6. 接口与默认方法
接口描述调用者可依赖的能力契约,可包含抽象方法、default、static 和 private 方法以及常量。类可实现多个接口;冲突 default 方法需显式解决。
工程理解: 接口按使用者需要划分,避免“万能 Service”;default 方法主要用于兼容演进。
常见误区: 把接口当作只有常量的容器,或让所有实现依赖庞大的接口。
7. 多态、向上转型与向下转型
子类引用赋给父类型是安全的向上转型;向下转型需保证实际对象兼容,否则抛出 ClassCastException。现代 instanceof Type variable 可同时判断并绑定变量。
工程理解: 大量类型判断通常说明多态边界或领域建模需要重构;sealed 类型配合 switch 可表达封闭状态集合。
常见误区: 只根据变量声明类型判断实际对象,或无检查地强制向下转型。
8. Object 契约
所有类最终继承 Object。equals 应满足自反、对称、传递、一致和非空;相等对象必须有相同 hashCode。toString 用于可读诊断,但不得泄露密码和令牌。
工程理解: 值对象同时实现 equals/hashCode;实体通常按稳定身份比较;可用 record 自动生成基于组件的实现。
常见误区: 只重写 equals 不重写 hashCode,导致 HashMap/HashSet 行为异常。
9. 成员、局部、静态与匿名内部类
非静态成员内部类持有外部实例;静态嵌套类不持有;局部类作用域限于块;匿名类创建一次性子类/接口实现。局部捕获变量必须是 final 或 effectively final。
工程理解: 无外部实例需求时优先 static 嵌套类;单方法接口的短行为优先 lambda。
常见误区: 非静态内部类意外延长外部对象生命周期,或用匿名类替代清晰可复用类型。
3.9 完整可运行练习:接口、多态与组合
保存为 PolymorphismDemo.java,执行 java PolymorphismDemo.java(JDK 21+)。下面只模拟展示价格,不连接真实支付服务。
public class PolymorphismDemo {
interface PriceRule { long price(long cents); }
static final class Regular implements PriceRule {
public long price(long cents) { return cents; }
}
static final class TenOff implements PriceRule {
public long price(long cents) { return Math.max(0, cents - 1000); }
}
static final class Checkout {
private final PriceRule rule;
Checkout(PriceRule rule) { this.rule = java.util.Objects.requireNonNull(rule); }
long quote(long cents) {
if (cents < 0) throw new IllegalArgumentException("金额不能为负");
return rule.price(cents);
}
}
public static void main(String[] args) {
System.out.println(new Checkout(new Regular()).quote(2500)); // 2500
System.out.println(new Checkout(new TenOff()).quote(2500)); // 1500
}
}
Checkout 保存的字段类型始终是 PriceRule,调用的却是实际对象实现的方法,这就是此处的动态分派。结算类通过持有规则对象实现组合,不必继承每一种价格规则,也不需要按具体类型写分支。
试着添加“满 5000 分减 1000 分”的实现,再测试 4999、5000、5001 三个边界。注意契约:上例约定 Checkout 会检查输入,规则实现必须对非负输入返回非负报价。接口写出方法形状,文字契约和测试补充编译器无法证明的业务含义。
4. 现代 Java 校准
- 使用
@Override、模式匹配instanceof、record 和 sealed class/interface 可以降低经典 OOP 样板。 - 接口的 default 方法不是多继承状态;它仍然不提供实例字段。
- 组合优先并非排斥继承,而是要求继承确实表达稳定子类型。
- HashMap/HashSet 中对象的相等字段在作为 key 期间应保持稳定。
5. 实践任务
- 为多种支付方式设计小接口,用组合替代 if/else 类型判断。
- 实现不可变 Money record,并测试 equals/hashCode。
- 制造一次错误的向下转型,再用模式匹配和 sealed hierarchy 消除。
6. 掌握检查
- 能区分重载、重写、隐藏和动态分派。
- 能说明接口与抽象类的选择条件。
- 能正确实现 equals/hashCode 契约。
- 能解释四类内部类对外部实例和局部变量的捕获。