☰
Java抽象类有什么用?从设计动机到面试考点一次讲透
2026/9/30 9:01:31 网站建设 项目流程

如果你在Java面试或日常开发里被问到“Java抽象类到底有什么用”,最常见的回答往往是“用来被继承的”。这个答案不能算错,但面试官基本不会满意,因为你只说了语法,没有说出设计动机。我带团队这些年有个很直观的感受:没理解抽象类的人,写出来的继承结构十有八九是为了继承而继承,要么把公共代码全塞进父类变成上帝类,要么让子类各自为政、重写全靠自觉。所以这篇博文不打算只罗列abstract关键字的规则,而是从一段大家几乎都写过的代码出发,把抽象类的来龙去脉、使用边界和面试考点一次讲透。适合正在准备Java基础面试、刚接触面向对象编程,以及写了好几年代码但没系统整理过抽象类知识的朋友。

1. 从“给所有动物加一声叫”开始:抽象类解决的真实痛点

抽象类不是Java语法里凭空冒出来的东西,它是在解决一个非常现实的问题:当我们面对一组“都算同一类事物”但“具体表现各不一样”的对象时,如何在父类里定义一个公共行为,同时强制每个子类都必须给出自己的实现。

1.1 一个平时看起来“能用就行”的例子

先来看一段没有任何抽象类的代码。假设系统里现在有猫和狗,我们需要它们都有叫唤的能力。常规操作是先写一个父类Animal,再让Cat和Dog分别继承:

class Animal { String name; void makeSound() { System.out.println(name + "发出某种声音"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("喵喵"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("汪汪"); } }

这段代码在刚写出来的时候没什么问题。Cat重写了makeSound,Dog也重写了makeSound,运行结果也符合预期。问题出在团队协作和系统扩展时。

假设你正在做一个动物收容所管理系统,新的需求来了,要加入Pig这个动物。新同事小张比较忙,他只继承了Animal,没有重写makeSound方法。这在编译阶段不会报任何错误,因为Animal的makeSound方法有一个默认实现,子类就算不重写也能通过编译。

等到系统上线,运维人员录入一只猪,点击“播放叫声”按钮,系统正常输出了“猪发出某种声音”。如果没有人知道猪的叫声应该是什么,这个输出看起来也没什么异常。直到产品经理来质问:“为什么猪的叫声是‘某种声音’?这个功能不是早就做完了吗?”

这时候你再回去看代码,就会发现问题出在父类的设计上:Animal类里的makeSound方法是可有可无的。它给了子类一个“退路”,让子类可以偷懒不实现,编译还不会提醒你。

1.2 没有强制约束时,继承体系会失控

“忘记重写”只是最轻微的失控。我在真实项目里见过更严重的情况:有人为了省事,直接把所有动物的公共逻辑全部堆到Animal里,比如把eat、sleep、move都写上默认实现,子类能重写就重写,不能重写就沿用父类。一开始看起来很高大上,半年之后这个Animal类膨胀到几千行,每个子类重写的方法都不全,有人依赖父类的默认行为,有人自己实现了一套逻辑。别人再往团队里安插新动物时,根本不知道哪些方法必须重写、哪些方法可以忽略。

更麻烦的是,这种问题往往会拖到运行时才暴露。数据结构、业务接口、序列化逻辑全都已经按“每个动物都有makeSound”来做了,结果某个动物因为漏了重写,行为就不符合预期。在Java这种强类型语言里,这种错误本该在编译期就被拦截下来,现在却要等测试环境甚至生产环境出问题。

普通类做不到这一点,就是因为普通类的方法必须写方法体,而方法体本身就成了“默认允许”的信号。遇到本来就应该由子类决定的抽象行为,直接把默认实现写在父类里,等于告诉后人:这里可以不重写,父类已经兜底了。

1.3 抽象类为什么能消灭这类问题

抽象类的做法完全不同。它把“必须有实现”这件事提升成了语法级别的约束。同一个动物例子,用抽象类来写:

abstract class Animal { String name; abstract void makeSound(); } class Cat extends Animal { @Override void makeSound() { System.out.println("喵喵"); } }

这段代码有两点关键变化。

第一,Animal被abstract修饰,不能直接new。你不能再去写new Animal()了,因为现实中不存在一只“泛泛的动物”,你只能new具体的子类。这个限制听起来很基础,却能避免很多无意义的实例化。

第二,makeSound方法只有声明、没有方法体,方法名后面直接用分号结束。在IDEA里如果你试图让Cat继承Animal但不实现makeSound,编译器会立刻标红:Cat不是抽象的, 并且未覆盖Animal中的抽象方法makeSound()。

这就把“必须重写”从口头约定变成了编译期强制规则。哪个新的动物类没实现叫声,代码根本编译不过去,提交到CI也会直接挂掉。与其上线后给产品经理解释为什么猪会发出“某种声音”,不如在写代码的时候就让编译器帮你把问题揪出来。

回到设计层面,抽象类的本质是:父类只定义“是什么”和“必须做什么”,至于“具体怎么做”,完全交给子类。这也是面向对象里最接近现实建模的做法——Animal这个概念本身就是抽象的,真实世界里的动物一定是猫、狗、猪这样具体的类别。

2. 抽象类语法背后的设计逻辑

抽象类的语法规则不多,但每一条规定背后都有它的理由。如果只是死记硬背“抽象方法不能有方法体”“抽象类不能new”,很快就会忘;理解为什么这么设计,面试和写代码都会顺畅得多。

2.1 abstract关键字到底约束了什么

abstract可以修饰类,也可以修饰方法。修饰类时,表示这个类是不完整的,不能通过new关键字创建实例;修饰方法时,表示这个方法也是一个不完整的声明,只有方法签名,没有方法实现,后面跟一个分号就结束。

需要特别注意的是,抽象方法不能和private、static、final组合使用。原因是这三者都会破坏“抽象方法交由子类实现”的语义。private方法对子类不可见,子类根本没法重写它;static方法属于类本身,不参与实例重写;final方法又禁止子类重写。这三个组合一旦放行,抽象方法就永远等不到子类来实现了,编译器的强制约束也就失去了意义。

还有一个大家容易忽略的点:一个类只要包含了抽象方法,哪怕只有一个,这个类就必须声明为abstract。反过来则不成立,抽象类可以没有抽象方法,它仍然不能直接new。现实中确实存在这种抽象类,后面我们会在模板方法的设计里看到类似应用。

2.2 抽象类可以有构造方法,但真正调用它的是子类

很多初学者听到“抽象类不能实例化”之后,会理所当然地认为抽象类没有构造方法。这个理解是错的。抽象类完全可以定义构造方法,甚至可以有多个重载构造方法。

那构造方法有什么用?既然不能new,难道是自己跟自己玩吗?当然不是。抽象类的构造方法主要用途是初始化抽象类中定义的那些实例字段。子类实例化时,JVM会先调用父类构造方法,再调用子类构造方法。即使你不在子类构造方法里写super(),编译器也会自动帮你补一个无参super()调用。

看一个例子:

abstract class Animal { protected String name; public Animal(String name) { this.name = name; } abstract void makeSound(); } class Cat extends Animal { public Cat(String name) { super(name); } @Override void makeSound() { System.out.println(name + ":喵喵"); } }

Cat的构造方法通过super(name)把参数传给Animal的构造方法,Animal把name保存到自己定义的受保护字段里。这样Cat实例创建时,name字段已经有了值,后续makeSound方法里可以直接使用。

有人会问,既然抽象类不能被实例化,它定义字段还有意义吗?意义非常大。子类继承抽象类后,这些字段就变成了子类对象的一部分。比如所有动物都有名称、年龄、体重,把这些公共状态放在抽象类里,可以避免每个子类重复声明同样的字段和setter/getter。这也是抽象类和接口之间一个本质区别:接口不能保存实例状态,抽象类可以。

2.3 静态方法、私有方法、final方法能不能进抽象类

这个问题在面试里出现的频率不低,很多人一听到抽象类就认为里面的方法都得是抽象的,其实不是。

抽象类完全可以包含静态方法,比如一个静态工厂方法,用来根据参数返回不同的子类实例。静态方法归属于类本身,可以在抽象类中正常写方法体,使用方式就是类名.方法名()。但静态方法不能用abstract修饰,因为abstract强制子类实现,而静态方法不参与继承重写。

抽象类也可以包含私有方法,用来封装一些单靠抽象类自己使用的公共私有逻辑。比如骨架流程里有一小段日志输出,不想暴露给子类,也不想在每个方法里重复写,就可以抽成一个私有方法。需要注意,Java 9之前接口里不允许有私有方法,但抽象类从第一天起就可以有私有方法,这是它的老本行。

抽象类里还可以定义final方法,表示这个方法在子类里不允许被重写。这个组合经常用在“模板方法”模式里:父类把流程骨架做成final,子类只能覆盖骨架中的个别步骤,不能整体篡改流程。比如下面的代码:

abstract class AbstractImporter { public final void importData() { openFile(); parse(); closeFile(); } protected abstract void parse(); private void openFile() { System.out.println("打开文件"); } private void closeFile() { System.out.println("关闭文件"); } }

importData方法被声明为final,子类只能继承这个流程,想重写整个导入流程是不被允许的。但parse方法留给子类自由发挥。这种组合能让抽象类在“固定框架”和“灵活扩展”之间取得很好的平衡。

3. 抽象类和接口怎么选:很多人栽在这里

抽象类和接口是Java里最容易混淆的一对概念。很多面试者都能背出“抽象类是is-a,接口是can-do”,但一到实际设计就不知道怎么选。这个选择不是靠一句口诀就能解决的,得结合字段、构造器、访问控制和设计语义一起看。

3.1 语义差异:is-a 与 can-do

抽象类表达的是“属于某种类型”的关系。Cat is an Animal,所以Cat继承Animal是合理的。接口表达的是“具备某种能力”的关系。飞机不是鸟,但它能飞,所以让Airplane实现Flyable接口更合适,而不是去继承某个Flyable抽象类。

最经典的例子是鸟类和飞行能力。如果把fly()方法放进Animal抽象类,然后让Bird继承Animal,那企鹅就会陷入一个尴尬局面:企鹅是鸟,但它不会飞。如果你在Animal里给fly()一个默认实现“拍打翅膀”,企鹅就变成一个会飞的企鹅了;如果你把fly()声明成抽象方法,企鹅被迫实现一个可能根本不用的方法。

正确做法是把飞行能力抽成接口:

interface Flyable { void fly(); } abstract class Animal { abstract void makeSound(); } class Sparrow extends Animal implements Flyable { @Override void makeSound() { System.out.println("叽叽喳喳"); } @Override public void fly() { System.out.println("麻雀飞行"); } } class Penguin extends Animal { @Override void makeSound() { System.out.println("企鹅叫声"); } }

这样企鹅不需要实现fly,麻雀既能拥有Animal的特性,又拥有Flyable的能力。这个例子虽然简单,但它传递的核心思想是:抽象类负责描述“这个东西是什么”,接口负责描述“这个东西能做什么”。一个类只能直接继承一个抽象类,但可以实现多个接口,所以当某个类型需要多种能力组合时,接口几乎是唯一选择。

3.2 状态和访问控制是抽象类的独有优势

接口在Java 8之后有了default方法,Java 9之后甚至允许私有方法,但它有一个无法绕过的问题:接口内部定义的所有字段都默认是public static final常量,接口本身不保存任何实例状态。

抽象类不一样。抽象类可以定义实例字段、私有字段、受保护字段,可以通过构造方法给这些字段赋值,还可以在字段变化时保证初始化顺序。这个能力在真实业务场景中太重要了。

举个例子,模拟支付渠道。每个支付渠道都有appId、privateKey这些配置信息,这些信息在渠道对象创建时就必须注入,而且是所有子类都需要维护的状态。如果用接口去定义,你只能写两个常量,但这个常量是全局共享的,无法做到每个实例各自保存一份;用抽象类,就可以把这些字段放在父类里,通过子类构造方法调用super(appId, privateKey)来初始化。

再考虑访问控制。接口中的方法,虽然可以写private方法,但接口的抽象方法默认为public,没办法声明成protected,因为实现接口的类可以在任何包中。抽象方法则可以是protected或public。当你不希望外部调用方直接访问某个扩展点,只希望子类内部实现时,protected抽象方法就非常有价值。它把“可以被谁重写”的控制权掌握在父类手里。

下面这张表可以帮你快速对比:

对比项抽象类接口
实例字段支持不支持,只能是public static final常量
构造方法支持不支持
方法访问修饰符public/protected/private均可Java 9后可以有private方法,但抽象方法仍为public
protected抽象方法支持不支持
多继承/多实现单继承多实现
默认方法实现普通方法default方法
设计语义is-a,注重代码复用和状态can-do,注重能力契约

3.3 Java 8引入默认方法后,选择变了吗

Java 8给接口增加了default方法,很多人的第一反应是“接口功能变强了,抽象类是不是要被淘汰了”。事实是,抽象类不仅没有被淘汰,它在某些场景下依然比接口更合适。

default方法确实解决了接口演进的问题——给已有接口新增方法时,不需要逼着所有实现类都去改,可以通过default提供默认实现。但它解决不了“状态共享”。接口里没有办法声明private String appId;这样的实例字段,也没有构造方法能接收外部参数,所以如果某个公共行为需要依赖内部状态,接口基本做不了。

模板方法模式就是一个典型例子。模板方法要求父类定义一个流程骨架,骨架里的步骤方法可能依赖父类字段。接口没有构造器和字段,即使能通过default方法写骨架,也没有地方保存骨架运行过程中需要的状态。抽象类则天然适合这种场景,后面我会专门展开。

所以现在的选型逻辑已经比较清晰:需要复用行为、共享状态、定义模板流程,优先选抽象类;需要定义能力契约、允许多个实现、希望调用方只依赖接口做解耦,优先选接口。两者还能搭配使用,后面会讲接口加抽象类的组合套路。

4. 抽象类在真实项目中的典型落地场景

理论讲太多容易飘,关键还得看真实项目里怎么用。抽象类在业务代码里最常见的三个场景,是模板方法、领域建模里的公共父类,以及“接口+抽象类”的组合模式。

4.1 模板方法模式:抽象类最经典的用法

模板方法模式是抽象类最经典、最不容易出错的用法。它的核心思想是:把一套固定的流程写在父类里,流程中不能确定的具体步骤,用抽象方法留给子类去实现。

我拿一个数据导入工具举例。假设系统需要从JSON文件、CSV文件、XML文件分别导入数据到数据库,它们的完整流程非常接近:

  1. 读取原始文件内容
  2. 把内容解析成统一的数据对象
  3. 校验数据
  4. 保存到数据库

这四步里,只有第一步和第二步的“具体格式处理”会因文件类型而变化,校验、存库这些步骤通常是公共的。于是可以写一个抽象父类:

public abstract class AbstractDataImporter { // 流程骨架,子类不允许篡改 public final void importData(String filePath) { String rawContent = readFile(filePath); List<DataModel> dataList = parse(rawContent); validate(dataList); save(dataList); } // 子类各自实现文件读取和解析 protected abstract String readFile(String filePath); protected abstract List<DataModel> parse(String rawContent); // 公共逻辑直接写在父类里 private void validate(List<DataModel> dataList) { if (dataList == null || dataList.isEmpty()) { throw new IllegalArgumentException("导入数据为空"); } for (DataModel model : dataList) { if (model.getId() == null) { throw new IllegalArgumentException("数据缺少ID"); } } } private void save(List<DataModel> dataList) { System.out.println("保存" + dataList.size() + "条记录"); } }

子类只需要关心自己擅长的部分:

public class JsonDataImporter extends AbstractDataImporter { @Override protected String readFile(String filePath) { return new String(Files.readAllBytes(Paths.get(filePath))); } @Override protected List<DataModel> parse(String rawContent) { return JsonUtil.parseList(rawContent, DataModel.class); } }

这样做的好处非常明显:整个导入流程只有一份,流程中的每一步都在父类里被严格串联;子类自己不需要知道先读文件还是先解析,那个顺序父类已经定死了。importData方法被final修饰后,子类也不能随意覆盖流程,防止有人图省事把整个流程复制一份去改。

4.2 领域建模中的公共父类+子类扩展

业务系统中经常有一组“表面相似但内部有差异”的实体,比如支付渠道中的微信支付、支付宝支付、银行直连支付。它们的业务逻辑高度相似:都有一组配置参数,都要发起支付、验签、查询订单,但每个渠道的签名算法和接口调用方式完全不同。

这种场景下,用抽象类做公共父类特别合适。把公共配置字段和公共流程放在父类,把各渠道差异点抽成抽象方法:

public abstract class AbstractPaymentChannel { protected String appId; protected String privateKey; public AbstractPaymentChannel(String appId, String privateKey) { this.appId = appId; this.privateKey = privateKey; } // 公共的支付入口,子类不能改写流程 public final String executePay(double amount) { String sign = buildSign(amount); return requestPay(amount, sign); } // 签名算法交给子类 protected abstract String buildSign(double amount); // 实际请求渠道接口交给子类 protected abstract String requestPay(double amount, String sign); }

子类实现微信支付时,只需继承这个抽象类,通过super(appId, privateKey)把配置传进去,然后分别实现buildSign和requestPay。以后要做新的支付渠道,只需要新建一个类继承AbstractPaymentChannel,没有任何人会担心它少做了某个步骤,因为抽象方法会逼着它做完。

这种建模方式的价值在大型系统和多人协作场景里特别明显。一个模块里有几十个类似业务类,如果全靠Copy Paste,改一个公共逻辑要同步改几十个地方;抽象类把公共逻辑收拢到一处,后续改支付超时时间、加统一日志,父类改一次,所有子类全部生效。

4.3 与接口混合使用的推荐组合

抽象类和接口不是二选一的对立关系,在实际项目中我更喜欢的做法是“接口定义能力,抽象类承接公共实现,具体类负责业务差异”。

这个组合通常长这样:

public interface PaymentChannel { void pay(double amount); } public abstract class AbstractPaymentChannel implements PaymentChannel { protected String appId; protected String privateKey; public AbstractPaymentChannel(String appId, String privateKey) { this.appId = appId; this.privateKey = privateKey; } @Override public final void pay(double amount) { String sign = buildSign(amount); requestPay(amount, sign); } protected abstract String buildSign(double amount); protected abstract void requestPay(double amount, String sign); } public class WechatPayChannel extends AbstractPaymentChannel { public WechatPayChannel(String appId, String privateKey) { super(appId, privateKey); } @Override protected String buildSign(double amount) { return "wechat-sign"; } @Override protected void requestPay(double amount, String sign) { System.out.println("调用微信支付接口"); } }

调用方只依赖PaymentChannel接口,不知道也不需要关心底层是哪个渠道。框架或配置中心通过反射或工厂返回具体实例,后续新增渠道不用改动调用方代码。而写新渠道的人,只需要继承AbstractPaymentChannel,实现两个抽象方法,其他的公共状态和流程一概不用管。

这个组合充分利用了接口的解耦能力和抽象类的复用能力,也是很多开源框架常见的分层方式:接口暴露给外部,抽象类做半成品,具体类完成业务闭环。

5. 面试题高频考点与常见误区排查

抽象类几乎是Java业务面试必考题,而且面试官非常喜欢在“看似简单”的问题里挖细节。下面这些问题,建议你在面试前能不看答案复述出来,并且最好能写几行代码验证。

5.1 面试官最爱问的抽象类问题

我整理了下面这个高频问题对照表,每一行都是面试现场真实出现过的:

面试题标准答案原因补充
抽象类能被实例化吗?不能抽象类是不完整的类,只能通过子类或匿名内部类间接使用
抽象类可以没有抽象方法吗?可以类被abstract修饰后就不能new,但可以没有抽象方法
有抽象方法的类必须是抽象类吗?必须类中有抽象方法但没声明abstract会编译报错
抽象类有构造方法吗?有构造方法用于子类实例化时初始化父类字段
抽象方法可以被static修饰吗?不行静态方法不参与重写,违背延迟到子类实现的语义
抽象方法可以被private修饰吗?不行private子类不可见,无法实现
抽象方法可以被final修饰吗?不行final禁止重写
抽象类能被final修饰吗?不行final类不能被继承,抽象类必须靠继承完成实现
子类可以不实现抽象方法吗?如果子类是抽象类,可以子类如果是具体类,必须实现所有抽象方法

这些问题听起来都是规则背诵,但面试官想考察的往往不是你记没记住规则,而是你能不能解释背后的Java语言设计动机。比如“为什么抽象方法不能static”,如果你能说到“static方法属于类、不参与实例重写,抽象方法需要子类实现”这一层,就说明你真的理解了。

5.2 五个容易翻车的细节(含代码演示)

第一个翻车点是匿名内部类和“实例化抽象类”的混淆。你确实能写出下面这种代码:

Animal animal = new Animal() { @Override void makeSound() { System.out.println("自定义动物叫声"); } };

从代码形式上看,好像是在用new创建抽象类的实例。实际上,这段代码创建的是一个继承Animal的匿名内部类的对象,它本质上是Animal的子类实例。抽象类本身依然没有被实例化。面试时如果有人问你“抽象类能不能通过匿名类来实例化”,最好把这一层说清楚,能加不少分。

第二个翻车点是抽象父类构造方法中调用了抽象方法,这是很多人在真实项目里踩过的运行时NPE。看下面这段代码:

abstract class Base { Base() { init(); } abstract void init(); } class Child extends Base { String name = "child"; @Override void init() { System.out.println(name.length()); } }

执行new Child()的时候,会先调用Base的构造方法,然后构造方法里调用init方法。此时Child的name字段还没来得及被赋值,仍然等于null,name.length()直接抛出NullPointerException。解决方法是不要在父类构造方法中调用任何可能被子类重写的方法,尤其是抽象方法。正确的做法要么把初始化逻辑放到子类构造方法中显式调用,要么用模板方法模式把初始化阶段放到流程的后半段。

第三个翻车点是abstract和final的冲突。有些人在写基类时,既希望这个类不能new,又不希望别人继承它,于是写了public abstract final class Foo。这个代码编译直接报错,因为abstract类必须通过继承来使用,final类又明确禁止继承,两个修饰符放在一起本身就是矛盾。你要想让类不能被实例化又不能被继承,通常会用私有构造方法加静态工厂方式实现。

第四个翻车点是对“子类不实现抽象方法”的处理。当子类没有实现父类全部抽象方法时,编译器会给出两种选择:要么把子类也声明成abstract,要么实现所有抽象方法。很多人在IDE提示下点了“Make child abstract”,编译瞬间通过,后来却发现那些原本new Child()的地方全部报错,因为Child已经变成抽象类,不能直接实例化了。这个坑在多人协作的代码库中出现的概率很高,改代码时一定要先看这个类有没有被外部实例化。

第五个翻车点是抽象类字段对序列化和框架的影响。很多RPC框架、JSON序列化库在反序列化对象时,需要调用类的无参构造器,或者需要父类有无参构造器。如果抽象父类只提供了带参构造方法,而且子类也没有显式声明无参构造方法,很可能会导致反序列化失败。遇到这种情况,要么在抽象父类中补充一个protected无参构造方法,要么在框架配置里指定合适的构造方法策略。

5.3 个人排错经验:抽象方法的契约比实现更重要

我在实际项目中印象最深的一个坑,倒不是语法问题,而是抽象方法的语义约束没定清楚。

有一次我们设计了一个报表导出功能,抽象父类里定义了一个抽象方法getExtraInfo(),Javadoc只写了一句话“获取额外信息”,没说明允许返回null。两个开发分别实现了两个子类,甲觉得自己超卖时额外信息为空很正常,直接return null;乙在公共流程里直接调用了getExtraInfo().length(),结果甲的子类一上线,导出报表时频频NPE,排错排了很久。

后来我把抽象类里的所有抽象方法都补上了严格的Javadoc契约:返回值是否允许null,参数是否允许null,是否允许抛异常,是否会阻塞。并且在模板方法里对容易出现null的返回做兜底判断,比如:

String extra = getExtraInfo(); if (extra == null) { extra = ""; }

这个经验看起来稀松平常,但在抽象方法特别多的类里非常管用。抽象方法本质上是父类和子类之间的一份约定合同,字段和方法签名只是合同的一半,行为约束才是另一半。很多团队写抽象类时只关注代码结构,忽略了抽象方法的行为文档,导致每个子类对“一个方法到底该怎么实现”的理解都不一样,最终代码越走越歪。

另外还有一条习惯性的检查点:如果一个抽象类的直接子类数量超过五六个,而且每个子类的实现差异都很大,我会回头重新审视抽象划分是否合理。抽象类适合收敛和管理共性,如果子类之间没有多少共性,只是强行套一个父类壳子,继承关系带来的耦合和修改成本很快就会超过它带来的便利。也许这时候更合适的是把公共代码抽成工具类,或者改用组合方式,把具体的可配置行为以策略对象的形式注入进来。

抽象类本身只是一个工具,真正决定代码质量的是使用工具的人是否清楚它的边界。每次我在代码评审里看到新的抽象类,都会先问三个问题:它是不是真的描述了子类之间稳定的公共特性?它的抽象方法是不是每一个都有清晰的行为契约?使用者是否已经有足够的理由选择继承而不是组合?如果三个问题都答不上来,这个抽象类大概率会在未来的某个迭代里变成重构的重点对象。抽象类的价值在于帮助你强制约定、复用状态、固化流程,但前提是你能把握住什么时候该用、什么时候别硬用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询