引言:为什么要学抽象类与接口
学完继承与多态之后,你已经会让「子类复用父类、父类引用调用子类方法」。可真实项目里还会遇到两类更「硬」的需求:
- 有些类不该被直接造出来
「员工」可以有全职、兼职,但「员工」本身往往只是一个抽象身份------你不会 `new Employee()`,只会 `new FullTimeEmployee()`。
- 有些能力要跨家族共享
「会飞」「能支付」「可序列化」不一定属于同一个继承树。鸟会飞,飞机也会飞------它们不是一类生物,却可以约定同一套「起飞」规范。
抽象类解决的是:半成品模板 ------共性状态 + 部分实现 + 强制子类补全。
接口解决的是:能力合同------约定「你必须会什么」,不强绑家族血统。
理解二者,你才能写出更易扩展、更低耦合的代码,也才真正迈向「面向接口编程」。

一、抽象类详解
1.生活画面:半成品图纸
把抽象类想成工厂类的【半成品图纸】:
| 生活中的画面 | 编程里的对应 |
|-------------|-------------|
| 图纸规定:都有外壳、都有电源接口 | 抽象类里的**成员变量 / 普通方法** |
| 有些工序写好了(组装外壳) | 抽象类里的**已实现方法** |
| 有些工序必须各型号自己完成(屏幕怎么贴) | **抽象方法**(子类必须实现) |
| 半成品本身不能出厂售卖 | **抽象类不能直接 `new`** |
| 只有具体型号才能量产 | **具体子类**完成抽象方法后才能实例化 |
一句话 :抽象类=【未完成的父类模板】它用来表达is-a,并强制子类补上关键差异。
2.使用场景
- 多个子类术语同一类事物,有共同字段 和部分相同逻辑;
- 希望提供默认实现,又强制子类实现某些方法;
- 需要再模板中写构造逻辑、保护字段、辅助方法。
典型例子:图形基类(公共坐标+抽象的area())、员工基类(姓名+抽象的pay())。
3.基本语法
java
package Exercise.blog;
public abstract class Shape {
public String name;
public Shape(String name) {
this.name = name;
}
// 普通方法:子类可直接用,也可重写
public void printName(){
System.out.println("图形"+name);
}
// 抽象方法:只有声明,没有方法体:子类必须实现
public abstract double area();
}
要点:
- 用abstract 修饰类;
- 含有抽象方法的类必须时抽象类;
- 抽象类可以没有抽象方法(少见,但合法、多用于【禁止直接实例化】).
4.抽象方法 VS 普通方法
| 对比项 | 抽象方法 | 普通方法 |
|--------|---------|---------|
| 方法体 | 无 | 有 |
| 修饰 | 必须 `abstract` | 无 `abstract` |
| 子类义务 | 必须实现(除非子类也是抽象类) | 可继承,可选重写 |
| 目的 | 强制约定差异行为 | 提供可复用默认逻辑 |
5.抽象类里能做什么?
| 成员 | 可不可以 | 说明 |
|------|---------|------|
| 成员变量 | ✅ | 可有实例字段、静态字段 |
| 构造方法 | ✅ | 供子类 `super(...)` 调用,不能用来 `new` 抽象类本身 |
| 普通实例方法 | ✅ | 代码复用的主力 |
| 抽象方法 | ✅ | 可有可无 |
| 静态方法 | ✅ | 属于类本身 |
| 直接实例化 | ❌ | `new Shape()` 会编译错误 |
6.完整可运行示例
java
public abstract class Shape {
protected String name;
public Shape(String name) {
this.name = name;
}
public void printName() {
System.out.println("图形:" + name);
}
public abstract double area();
}
public class Circle extends Shape {
private double radius;
public Circle(String name, double radius) {
super(name);
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String name, double width, double height) {
super(name);
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
}
public class AbstractDemo {
public static void main(String[] args) {
// Shape s = new Shape("x"); // 编译错误:抽象类不能实例化
Shape c = new Circle("圆", 2);
Shape r = new Rectangle("矩形", 3, 4);
c.printName();
System.out.println("面积 ≈ " + c.area());
r.printName();
System.out.println("面积 = " + r.area());
}
}
运行结果:
图形圆
面积 = 12.566370614359172
图形矩形
面积 = 12.0
为什么这样设计?
printName()所有图形都一样,放进抽象类复用;area()算法各不同,用抽象方法强制子类各自实现。父类引用Shape 调用area(), 仍然是多态。

二、接口详解
1.生活画面:能力合同
把接口想成一份【能力合同】
| 生活中的画面 | 编程里的对应 |
|-------------|-------------|
| 合同写明:必须会「支付」「退款」 | 接口中的**抽象方法** |
| 不关心你是支付宝还是银行卡 | 实现类可以来自**不同继承树** |
| 签约方按合同办事 | `implements` 后实现方法 |
| 合同可升级:提供默认条款 | Java 8+ 的 `default` 方法 |
| 合同附带工具条款 | `static` 方法 |
| 合同里的固定条款 | `public static final` 常量 |
一句话:接口描述can-do(能做什么),强调规范,不强调家族血统。
2.设计目的
- 定义统一行为规范,让调用方法依赖【能力】而非【具体类】;
- 支持一个类拥有多种能力(多实现);
- 降低模块耦合,便于替换实现(换支付渠道、换消息通道)。
3.声明与实现语法
java
public interface Payable {
// 常量:默认 public static final
int MAX_RETRY = 3;
// 抽象方法:默认 public abstract
boolean pay(double amount);
// Java 8+:默认方法,实现类可不重写
default void printReceipt(double amount) {
System.out.println("已支付:" + amount + " 元");
}
// Java 8+:静态方法,通过接口名调用
static void tip() {
System.out.println("请确认支付金额");
}
}
public class Alipay implements Payable {
@Override
public boolean pay(double amount) {
System.out.println("支付宝支付 " + amount);
return true;
}
}
版本提示:
- Java7 及更早:接口里主要是抽象方法和常量;
- Java8: 引入default、static 方法;
- Java9+:还可有private 方法,供默认方法复用内部逻辑。
4.一个类实现多个接口
Java类只能extends一个类,但可以impeachment 多个接口:
java
public interface Flyable {
void fly();
}
public interface Swimmable {
void swim();
}
public class Duck implements Flyable, Swimmable {
@Override
public void fly() {
System.out.println("鸭子飞起来了");
}
@Override
public void swim() {
System.out.println("鸭子游泳了");
}
}
生活类比:一个可以同时签【会开车】【会游泳】两份能力合同,但不等于有两个亲生父亲。
5.接口之间可以继承(可多继承)
java
public interface Movable {
void move();
}
public interface Flyable extends Movable {
void fly();
}
实现 `Flyable` 的类,需要实现 `move()` 和 `fly()`(除非有默认实现)。
6.完整可运行示例
java
public interface Notifier {
void send(String message);
default void sendWithPrefix(String message) {
send("[通知] " + message);
}
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("短信发送:" + message);
}
}
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("邮件发送:" + message);
}
}
public class InterfaceDemo {
public static void main(String[] args) {
Notifier sms = new SmsNotifier();
Notifier email = new EmailNotifier();
sms.sendWithPrefix("订单已发货");
email.sendWithPrefix("订单已发货");
}
}
运行结果:
短信发送:通知 订单已发货
邮件发送:通知 订单已发货
调用方只依赖 `Notifier`,换短信/邮件实现无需改业务主流程------这就是面向接口编程的价值。

三、抽象类与接口的核心区别
| 对比项 | 抽象类 | 接口 |
|--------|--------|------|
| 继承 / 实现 | `extends` 单继承 | `implements` 可多实现 |
| 是否支持多继承 | 类不支持多继承 | 接口可多继承接口 |
| 构造方法 | 可以有 | **不可以有** |
| 成员变量 | 可有普通实例变量等 | 通常是 `public static final` 常量(Java 8 前印象最深);字段语义偏「常量约定」 |
| 方法类型 | 抽象方法 + 普通方法 + 静态方法等 | 抽象方法 + `default` + `static`(Java 8+)+ `private`(Java 9+) |
| 访问修饰符 | 方法可用多种可见性 | 抽象方法默认 `public`;`default`/`static` 也有规则限制 |
| 适用场景 | 同一家族,共享状态与部分实现 | 跨类型共享能力规范 |
| 与子类 / 实现类关系 | **is-a**(是一种) | **can-do**(能做什么) |
| 代码复用能力 | 强:字段 + 已实现方法直接复用 | 中:靠默认方法复用行为,不适合承载大量可变状态 |
| 设计思想 | 模板化、半成品扩展 | 契约化、解耦、面向能力 |
抽象类像「半成品手机图纸」(有电池仓尺寸、有部分工序);
接口像「必须支持快充的行业标准」(谁符合标准都能接)。
四、综合案例:支持与订单通知
1.业务需求
做一个简易电商片段:
- 订单有公共信息(订单号、金额),支付流程有固定步骤(校验->扣款->记录),但具体支付渠道不同;
- 支付成功后要发通知,通知方式可能是短信或邮件,且与支付渠道无继承关系。
因此:'
- 抽象类承载【订单支付模板】与公共状态;
- 接口承载【可通知能力】。
2.设计图

3.完整代码
java
/** 支付渠道:同一家族,共享流程模板 + 强制差异实现 */
public abstract class PaymentChannel {
protected String channelName;
public PaymentChannel(String channelName) {
this.channelName = channelName;
}
/** 模板方法:固定步骤,差异交给子类 */
public final boolean payTemplate(String orderId, double amount) {
if (amount <= 0) {
System.out.println("金额非法");
return false;
}
System.out.println(channelName + " 开始支付,订单:" + orderId);
boolean ok = doPay(amount);
if (ok) {
System.out.println(channelName + " 支付成功,已记账");
} else {
System.out.println(channelName + " 支付失败");
}
return ok;
}
/** 子类实现真正的扣款逻辑 */
protected abstract boolean doPay(double amount);
}
public class AlipayChannel extends PaymentChannel {
public AlipayChannel() {
super("支付宝");
}
@Override
protected boolean doPay(double amount) {
System.out.println("调用支付宝 SDK,扣款 " + amount);
return true;
}
}
public class WechatChannel extends PaymentChannel {
public WechatChannel() {
super("微信支付");
}
@Override
protected boolean doPay(double amount) {
System.out.println("调用微信支付 API,扣款 " + amount);
return true;
}
}
/** 通知能力:跨模块合同,不绑死某个父类 */
public interface Notifier {
void send(String message);
default void sendSuccess(String orderId) {
send("订单 " + orderId + " 支付成功");
}
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[短信] " + message);
}
}
public class EmailNotifier implements Notifier {
@Override
public void send(String message) {
System.out.println("[邮件] " + message);
}
}
/** 业务服务:依赖抽象与接口,而不是具体实现 */
public class OrderService {
private final PaymentChannel channel;
private final Notifier notifier;
public OrderService(PaymentChannel channel, Notifier notifier) {
this.channel = channel;
this.notifier = notifier;
}
public void checkout(String orderId, double amount) {
if (channel.payTemplate(orderId, amount)) {
notifier.sendSuccess(orderId);
}
}
public static void main(String[] args) {
OrderService s1 = new OrderService(new AlipayChannel(), new SmsNotifier());
s1.checkout("A1001", 99.0);
System.out.println("---");
OrderService s2 = new OrderService(new WechatChannel(), new EmailNotifier());
s2.checkout("A1002", 128.5);
}
}
运行结果:
**支付宝支付 开始支付,订单:A1001
调用支付宝 SDK, 扣款 99.0
支付宝支付支付成功,已记账
短信:订单 A1001支付成功微信支付 开始支付,订单:A1002
调用微信支付API,扣款 165.0
微信支付支付成功,已记账
邮件:订单 A1002支付成功**
4.设计思路拆解
| 设计点 | 为什么用抽象类 / 接口 |
|--------|----------------------|
| `PaymentChannel` | 支付渠道是一类事物,有共同字段 `channelName` 和固定流程 `payTemplate` → **抽象类** |
| `doPay` | 各渠道实现不同 → **抽象方法** |
| `Notifier` | 短信/邮件与支付渠道无 is-a 关系,只是「能通知」→ **接口** |
| `OrderService` | 依赖 `PaymentChannel` 与 `Notifier`,方便替换组合 |
这就是实际开发里最常见的搭配:抽象类管模板与状态,接口管可插拔能力。
五、常见误区与注意事项
1.抽象类不能直接创建对象
java
// Shape s = new Shape("x"); // 错误
Shape s = new Circle("圆", 1); // 正确
半成品不能当成品卖。
2.抽象类不一定必须包含抽象方法
java
public abstract class SystemUser {
private String id;
// 没有抽象方法,但也不能直接 new
}
用途:强制只能通过子类实例化。不过更常见的是【有抽象方法】。
3.实现接口的类必须实现抽象方法(除非自己也是抽象类)
java
public abstract class AbstractNotifier implements Notifier {
// 可以暂不实现 send(),继续抽象
}
public class SmsNotifier implements Notifier {
@Override
public void send(String message) { /* 必须实现 */ }
}
4.一个类可实现多个接口,但只能继承一个类
java
class SmartDuck extends Bird implements Flyable, Swimmable { }
这是Java但继承 + 多实现的基本盘
5.默认方法冲突要手动解决
当两个接口有同名default方法,实现类必须重写并选择/合并:
java
interface A {
default void hello() { System.out.println("A"); }
}
interface B {
default void hello() { System.out.println("B"); }
}
class C implements A, B {
@Override
public void hello() {
A.super.hello(); // 显式指定用哪个
// 或写自己的逻辑
}
}
6.不要为了用而用
如果只有一个实现、没有复用、没有扩展预期,险些具体类往往更清新。
抽象类和接口是为了表达约束与扩展点,不是为了让类图看起来怕【高级】。
六、选择建议(可执行口诀)
| 场景 | 更建议 |
|------|--------|
| 多个子类有共同状态 + 部分相同实现 | **抽象类** |
| 多个不相关的类需要相同行为 | **接口** |
| 一个类需要多种能力(飞、游、支付...) | **接口(可多实现)** |
| 要定义统一规范、降低模块耦合、便于替换 | **接口**(常配合依赖注入思想) |
| 需要模板流程(固定步骤 + 差异步骤) | **抽象类**(常配合模板方法) |
| 既要模板,又要额外插件能力 | **抽象类 + 接口** 组合(见综合案例) |
口诀:
- 有血统、有状态、要半成品->抽象类;
- 要能力、要解耦、要多实现->接口。
七、总结
| 概念 | 一句话 |
|------|--------|
| 抽象类 | 不能直接实例化的半成品父类,适合 is-a + 代码复用 |
| 抽象方法 | 只有声明,强迫子类补实现 |
| 接口 | 能力合同,适合 can-do + 多实现 + 解耦 |
| `default` 方法 | 给接口演进提供默认实现(Java 8+) |
| 面向接口编程 | 依赖抽象规范,而不是依赖具体实现类 |
它们和封装、继承、多态的关系**:**
封装: 隐藏具体支付/通知细节**;**
**继承:**抽象类继续表达家族复用;
**多态:**通过抽象类引用或接口引用调用不同实现。