27. 面向对象基础

系统掌握面向对象核心:封装(访问控制/信息隐藏)、继承(is-a 与组合优先)、多态(重载/重写/接口多态)、抽象类与接口的区别、SOLID 设计原则、组合优于继承、以及 OOP 在工程中的设计取舍与常见反模式。

1. 三大基本特征

1.1 封装、继承、多态

面向对象编程(OOP)的三大特征是封装、继承、多态:

# 封装: 把数据与操作数据的方法绑定在一起,对外隐藏内部实现
# 继承: 子类复用/扩展父类的属性和方法(is-a 关系)
# 多态: 同一接口在不同实现下表现不同行为

三大特征的工程意义:封装降低耦合(改动局部化),继承促复用(消除重复),多态支撑扩展(开闭原则)。但三者都不可滥用——过度继承是架构腐化的头号来源。

2. 封装与访问控制

2.1 信息隐藏

封装的本质是信息隐藏:把「内部状态」藏起来,只暴露「受控的操作接口」。对外面的代码,对象是一堆行为,而不是一堆可乱改的字段。

# 反模式: 直接暴露可变字段
# obj.count += 1          // 绕过校验与约束
# 正模式: 通过方法操作
# obj.increment()         // 内部可加校验/同步/日志

2.2 访问修饰符的选用

修饰符可见范围适用
public全部对外接口
protected子类 + 包内供扩展的钩子
默认(包)包内内部协作
private类内内部实现

工程建议:字段一律 private,暴露用方法/属性;只有确需子类重写的才用 protected。访问控制越严,改动的冲击面越小。

3. 继承:is-a 与陷阱

3.1 is-a 关系

继承表达 is-a:子类是父类的特化(Dog is-a Animal)。合理的继承是「子类在父类能力上增加/细化」,不合理的继承是「为了复用代码而强行继承」。

# 合理继承: 子类扩展行为
# class Circle extends Shape { override draw() {...} }
# 不合理继承: 只为用父类方法
# class Stack extends ArrayList  // 栈不是"一种"ArrayList!破坏 LSP

3.2 继承的两个经典问题

  • 脆弱的基类:父类一改,所有子类行为漂移,难以追责。
  • 破坏封装:子类访问父类 protected 字段,父类内部约定被侵入。
  • 菱形继承:多继承下同一父类被继承两次(C++ 需虚拟继承;Java 用接口规避)。

规避策略:优先组合(持有对象的引用)而非继承——组合表达的「has-a」更弱耦合、更易测试、更好演化。

4. 多态:重载与重写

4.1 编译期与运行期多态

# 重载(overload): 同名方法、不同参数 —— 编译期决定(静态多态)
# 重写(override): 子类重新实现父类方法 —— 运行期决定(动态多态)
# 多态实现条件: 继承 + 重写 + 父类引用指向子类对象
# Animal a = new Dog(); a.speak();  // 实际调用 Dog.speak()

多态的价值:面向接口编程。调用方依赖抽象(父类/接口),不依赖具体实现——替换实现时调用方不用改。这是开闭原则的基石。

4.2 多态的底层

动态多态靠虚方法表(vtable):每个类一张表,记录方法指针,调用时查表定位实际方法。理解这一点对「为什么接口调用比直接调用略慢」「为什么静态方法没有多态」有帮助。

5. 抽象类与接口

5.1 各自定位

抽象类接口
继承关系is-a(类继承)can-do / 契约(实现)
状态可有实例字段无状态(Java8+ 可有 default 方法)
多继承单继承可多实现
演进加方法会破坏子类default 方法可平滑扩展
# 选型: 需共享状态/实现 → 抽象类;纯契约/多实现 → 接口
# 现代工程趋势: 接口为主,抽象类为辅(多用接口声明能力)

5.2 接口即契约

接口表达「能做某件事」的能力契约。依赖抽象(接口)而非具体类,是依赖倒置原则(SOLID 的 D)的实现手段——高层模块依赖抽象,低层模块实现抽象。

6. SOLID 设计原则

6.1 五大原则速览

  • S(单一职责):一个类只负责一件事。改动原因唯一。
  • O(开闭原则):对扩展开放、对修改关闭。新功能加代码不删改旧逻辑(多态 + 策略)。
  • L(里氏替换):子类必须能替换父类而不破坏程序。继承必须保持 is-a 语义。
  • I(接口隔离):接口小而专,不强迫实现类实现用不到的方法。
  • D(依赖倒置):依赖抽象,不依赖具体实现。
# 反例(违反 OCP)
# if (type == "email") sendEmail(); else if (type == "sms") sendSms();
# 正例(多态 + 策略)
# 每个发送渠道实现 Sender 接口,新渠道加一个实现类
# 调用方遍历 Sender 列表即可 —— 加渠道不改调用方

SOLID 的落地价值:让「加功能」从「改旧代码」变成「加新代码」,降低回归风险与耦合。

7. 组合优于继承

7.1 组合的优势

# 组合: class Bird { Flyable fly; }   // has-a 能力
# 继承: class Bird extends Animal     // is-a 关系
# 组合的好处: 灵活换实现、弱耦合、易测试(可注入 mock)
# 何时坚持继承: 真正 is-a + 需要复用父类实现 + 不破坏 LSP

经验法则:默认组合,确需继承才继承。先问「子类真的是一种父类吗」,「能复用父类实现吗」,「换成组合会不会更简单」——多数场景组合胜出。

7.2 装饰器模式(组合的典范)

装饰器用「组合 + 递归包装」扩展对象能力,替代深继承链:new BufferedInputStream(new FileInputStream(...)) 就是装饰器——层层包装、能力叠加,无需为每种组合写一个子类。

8. 常见反模式

  • 上帝类(God Class):一个类塞满所有职责,违背单一职责。拆分。
  • 继承替代组合:为复用写 is-a,一旦需求变化基类成了枷锁。
  • 过度抽象:为「可能扩展」建一堆接口,实际只有一个实现。抽象要为「真实变化」预留,不为臆想。
  • 贫血模型:类只有 getter/setter 无行为,退化成数据类——行为放哪了?(领域逻辑应尽量内聚进对象。)

9. 工程实践清单

# OOP 落地检查单
# 1) 字段 private,行为走方法
# 2) 依赖接口/抽象,不依赖具体类
# 3) 默认组合,继承只在真 is-a 且不破坏 LSP
# 4) 新功能优先加实现类,不改旧代码(OCP)
# 5) 一个类讲一个故事(SRP)
# 6) 接口小而专(ISP),抽象为真实变化预留

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 28. 网络应用层协议深入
  2. 26. 栈、队列与堆
  3. 25. IO 模型与多路复用