3.1 半成品档案:抽象类


3.1 半成品档案:抽象类

本节摘要:抽象类是被 abstract 修饰的半成品档案——可以装字段、构造器与具体方法,也能开出 abstract 空栏;它自己不能实例化,具体子类必须补全全部空栏才能落户。抽象类换来的核心价值是"模板方法":父类定死流程骨架,把个别步骤留成空栏交子类填写。本节拆解抽象成员的规则与"有构造器却不能 new"这对矛盾。

为什么要把父类做成"不完整的"

回到第 2 章的 Staff 例子再往前走一步。调度器持有 Staff 引用调用 duty,运行没问题,但有个漏洞:谁都可以 new Staff(),造出一个"什么职责都没有的通用员工"——业务上这是无效数据,可编译器拦不住。更深一层的问题:duty 在父类里该写什么?写空实现,子类忘了重写就静默输出空白;写通用文案,实习生打印出来就是错的。两种写法都是在给"不该有默认答案的问题"硬编答案。

抽象类的解法是把这类问题显式化:方法只签名、不写方法体,标 abstract;含抽象方法的类必须整体标 abstract,编译器禁止它被实例化

abstract class Staff { // 半成品档案:不能直接 new protected String name; protected int no; Staff(String name, int no) { // 抽象类可以有构造器 this.name = name; this.no = no; } abstract void duty(); // 空栏:只签名 不给答案 void checkIn() { // 具体方法:家族通用流程 直接继承 System.out.println(no + " 号 " + name + " 已打卡"); } } class Engineer extends Staff { Engineer(String name, int no) { super(name, no); } @Override void duty() { // 子类必须补栏 否则自己也得是抽象类 System.out.println("工程师:开发与排查缺陷"); } } public class AbstractDemo { public static void main(String[] args) { // Staff s = new Staff("无名", 0); // 编译报错:抽象类不能实例化 Staff e = new Engineer("郑一", 3001); // 多态:父类引用指向具体子类 e.checkIn(); // 输出:3001 号 郑一 已打卡 e.duty(); // 输出:工程师:开发与排查缺陷 } }

有三条规则从这段代码里长出来。其一,空栏强制补全:具体子类必须实现父类全部抽象方法,漏一个就编译报错——这比 2.1 节"靠自觉重写"硬气得多,漏写在编译期就暴露。其二,抽象类可以有构造器但不能被 new——这对看似矛盾的表述其实是配合关系:构造器不是给外部用的,是给子类构造器里那句 super 用的(2.1 节入籍链)。抽象父类的字段初始化,必须有人代劳,代劳者就是子类构造器。其三,抽象类可以没有抽象方法:整体标 abstract 单纯为了禁止实例化(比如某些工具基类),但含抽象方法的类必须标 abstract——不标的话,外面 new 出来、调用一个没有方法体的方法,运行时就乱了。

模板方法:把流程骨架焊死在父类

抽象类最大的工程价值是模板方法:父类用一个具体方法把流程步骤串成骨架,其中"人人相同的步骤"直接写实现,"各家不同的步骤"留成空栏。子类只填空栏,不许动骨架——骨架方法通常加 final 封口(1.4 节)。

abstract class Report { // 上报流程:骨架固定 步骤可定制 public final void submit() { // final 封口:流程顺序不许子孙改动 collect(); // 第一步 收集数据(各家不同) System.out.println("数据已汇总,格式化为统一报表"); audit(); // 第二步 审核(各家不同) System.out.println("上报完成,已归档"); } protected abstract void collect(); // 空栏一 protected abstract void audit(); // 空栏二 } class DailyReport extends Report { @Override protected void collect() { System.out.println("拉取当日业务流水"); } @Override protected void audit() { System.out.println("主管当日复核"); } } class MonthReport extends Report { @Override protected void collect() { System.out.println("聚合当月台账并计算环比"); } @Override protected void audit() { System.out.println("财务与合规双线审核"); } } public class TemplateDemo { public static void main(String[] args) { new DailyReport().submit(); // 输出:拉取当日业务流水 / 数据已汇总,格式化为统一报表 / 主管当日复核 / 上报完成,已归档 new MonthReport().submit(); // 输出:聚合当月台账并计算环比 / 数据已汇总,格式化为统一报表 / 财务与合规双线审核 / 上报完成,已归档 } }

看输出就能读出骨架与空栏的分工:第一、三行随子类变化,第二、四行永远不变。完整案例背景:某系统每天、每月、每季度各有一套上报代码,流程被复制三遍,后来合规要求"所有上报必须先审核后归档",三处代码改了两处漏了一处,被审计点名。操作:抽取 Report 抽象类如上,流程 submit 用 final 焊死,把易变步骤留成空栏。结果:合规变更只需改父类一处,三类报表同时生效;新增季度报表只需填两个空栏。解读:模板方法把"变化"与"不变"分离——不变的流程下沉到父类并封口,变化的步骤上抛给子类。变式:若流程某个步骤"多数子类相同、个别不同",把空栏改成带默认实现的具体方法,个别子类再重写——空栏数量按需调节,不必为统一而统一。

抽象类的边界

抽象类不是万能锤,它的边界来自单继承:一个类只能入一个族谱,抽象父类占掉的这个名额很贵。所以判断标准是——你要复用的是"家族级模板"(状态、构造器、流程骨架)还是"能力契约"(会做什么)。前者用抽象类,后者用接口(下一节)。JDK 里的实例:输入输出框架的抽象基类为各种流定骨架,集合框架的抽象列表类替实现类干掉大部分通用方法——都是"模板"语义;而 Comparable、Serializable 这类是纯资格,用接口实现。

还有一条容易被忽略的细节:抽象方法不能与 private、final、static 同用——private 让子类看不见、final 禁止重写、static 不参与动态绑定,三者都与"等着子类来填"的语义直接冲突。理解这条禁令最好的办法是把 2.2 节动态绑定的图再翻出来看一遍:空栏的实现靠的就是运行期按实际对象找方法表。

本节要点回顾

  • 抽象类是半成品档案:可以装字段、构造器与具体方法,也能开 abstract 空栏;自己不能实例化,具体子类必须补全全部空栏
  • 有构造器与不能 new 并不矛盾:构造器是给子类 super 调用的,负责父类字段的入籍;实例化的门只对具体子类开放
  • 模板方法是抽象类的主战场:final 流程骨架加 abstract 可变步骤,把不变与变化分离,合规改动收敛到一处
  • 空栏强制补全:漏实现抽象方法编译期报错,比"靠自觉重写父类方法"可靠一个量级
  • 抽象方法与 private final static 互斥:三种修饰都掐断了"子类填写"的通道,语义自相矛盾

抽象类锁定了"家族模板",但一个类只能入一本族谱。如果需要的是"持有多张资格证"——可比较、可序列化、会飞——就得换一种登记物:下一节的接口,全册最灵活的契约机制。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U