第 10 课:接口、依赖抽象与策略模式入门
本课目标
学完后,你应该能够:
- 使用
interface声明行为协议。 - 使用
implements实现接口,并正确重写接口方法。 - 理解接口抽象方法隐含的
public abstract。 - 使用接口的
default方法和static方法。 - 让一个类同时实现多个接口。
- 使用接口引用、接口数组和接口参数实现多态。
- 理解接口不能保存对象实例状态,也没有构造方法。
- 区分接口与抽象类的职责。
- 让业务类依赖抽象,而不是写死具体实现。
- 理解策略模式与依赖注入的基本思想。
1. 接口是什么
接口描述“实现者必须具备什么能力”,它关注行为协议,而不是某一类对象共享的内部状态。
public interface PaymentMethod {
String getName();
boolean pay(double amount);
}
这个接口规定:任何支付方式都必须能提供名称并尝试支付。它没有规定支付宝、微信或银行卡具体怎样扣款。
实现类使用 implements:
public class AlipayPayment implements PaymentMethod {
@Override
public String getName() {
return "支付宝";
}
@Override
public boolean pay(double amount) {
return true;
}
}
关系可以读作:“AlipayPayment 实现了 PaymentMethod 规定的支付能力。”
2. 接口方法的隐含修饰符
接口中的普通抽象方法:
boolean pay(double amount);
等价于:
public abstract boolean pay(double amount);
因此实现方法必须是 public:
@Override
public boolean pay(double amount) {
return true;
}
如果省略 public,实现方法只有包级可见性,比接口要求的公开权限更小,无法正确实现接口。
3. 接口不能直接创建对象
PaymentMethod method = new PaymentMethod(); // 错误
接口没有完整实现,不能直接实例化。但可以声明接口引用并指向实现对象:
PaymentMethod method = new AlipayPayment(...);
这与抽象父类引用指向子类对象相似:
- 编译时根据
PaymentMethod判断可调用成员; - 运行时根据实际
AlipayPayment对象执行实现。
4. 接口没有实例字段和构造方法
接口不能保存每个对象不同的余额、账号或限额,也没有构造方法。具体状态放在实现类中:
public class AlipayPayment implements PaymentMethod {
private String account;
private double balance;
}
接口中声明的字段自动是:
public static final
也就是公共静态常量:
public interface Example {
int MAX_RETRY = 3;
}
等价于:
public static final int MAX_RETRY = 3;
它属于接口本身,所有实现共享,不能保存某个对象自己的可变状态。
5. default 默认方法
Java 8 开始,接口可以提供默认实现:
default boolean isValidAmount(double amount) {
return amount > 0;
}
实现类会继承这个方法:
if (!isValidAmount(amount)) {
return false;
}
默认方法的作用:
- 给多个实现类提供共享行为;
- 给已有接口添加新方法时提供默认实现,减少所有实现同时修改的压力;
- 实现类仍可以使用
@Override替换默认行为。
默认方法依然是实例方法,可以参与多态。
6. 接口静态方法
static String formatAmount(double amount) {
return String.format("%.2f", amount);
}
调用时使用接口名:
PaymentMethod.formatAmount(88.5);
接口静态方法属于接口本身,不属于实现对象,不通过实现类继承,也不参与运行时多态。
错误思路:
paymentMethod.formatAmount(88.5);
应始终通过 PaymentMethod.formatAmount() 表达它属于接口工具能力。
7. 接口私有方法
Java 9+ 允许接口使用 private 方法,供多个默认方法内部复用:
public interface Example {
default void first() {
printPrefix();
}
default void second() {
printPrefix();
}
private void printPrefix() {
System.out.println("公共前缀");
}
}
接口私有方法不能由实现类或外部调用,只用于整理接口内部默认逻辑。本课案例暂不需要它,但要知道这种语法存在。
8. 一个类实现多个接口
Java 类只能直接继承一个父类,但可以实现多个接口:
public class AlipayPayment implements PaymentMethod, Refundable {
}
这表示支付宝对象同时具有支付能力和退款能力:
PaymentMethod payment = alipay;
Refundable refundable = alipay;
同一个对象可以从不同接口视角被使用。接口适合描述可组合的能力,例如:
Comparable:可以比较;Runnable:可以运行任务;AutoCloseable:可以关闭资源;Serializable:可以序列化。
类还可以先继承一个父类,再实现接口:
public class SpecialPayment extends BasePayment
implements PaymentMethod, Refundable {
}
语法顺序必须是 extends 在前、implements 在后。
9. 接口之间的继承
接口使用 extends 继承其他接口,而且可以继承多个接口:
public interface AdvancedPayment
extends PaymentMethod, Refundable {
}
实现 AdvancedPayment 的类必须满足它和所有父接口的协议。
10. 默认方法冲突
如果两个接口提供同名同参数的默认方法,实现类必须明确解决冲突:
interface First {
default void show() {
System.out.println("First");
}
}
interface Second {
default void show() {
System.out.println("Second");
}
}
class Demo implements First, Second {
@Override
public void show() {
First.super.show();
Second.super.show();
}
}
First.super.show() 表示明确调用某个接口的默认实现。
11. 接口多态
PaymentMethod method = new AlipayPayment(...);
method.pay(100.0);
method = new WeChatPayment(...);
method.pay(100.0);
变量类型一直是 PaymentMethod,真实对象可以更换。调用 pay() 时运行实际实现类的方法。
接口数组:
PaymentMethod[] methods = {
new AlipayPayment(...),
new WeChatPayment(...)
};
for (PaymentMethod method : methods) {
method.pay(100.0);
}
接口参数:
public void checkout(PaymentMethod method, double amount) {
method.pay(amount);
}
调用者传入什么实现,运行时就执行什么策略。
12. 依赖具体类的问题
写死支付宝:
public class PaymentService {
private AlipayPayment payment;
}
这样 PaymentService 只能使用支付宝。想改微信,就要修改字段、构造方法甚至业务逻辑。
依赖接口:
public class PaymentService {
private PaymentMethod paymentMethod;
public PaymentService(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
}
现在业务类只依赖“能支付”这项协议,支付宝、微信、银行卡或未来的新实现都可以传入。
这叫依赖抽象。后续 Spring 最核心的依赖注入思想就是:业务组件声明自己需要哪个接口,由容器提供具体实现。
13. 策略模式
策略模式把一组可以互换的算法或行为封装成不同实现,并让使用者在运行时选择。
本课映射:
策略接口:PaymentMethod
具体策略:AlipayPayment、WeChatPayment
上下文: PaymentService
客户端: PaymentDemo
调用关系:
PaymentDemo
↓ 传入某种策略
PaymentService ───依赖──→ PaymentMethod
↑
┌─────────┴─────────┐
AlipayPayment WeChatPayment
切换策略:
paymentService.setPaymentMethod(weChat);
checkout() 不需要增加 if/else 判断具体支付方式。
14. 接口与抽象类的区别
| 对比 | 接口 | 抽象类 |
|---|---|---|
| 核心角色 | 描述能力和协议 | 表达一族对象的共同基础 |
| 关键字 | interface / implements | abstract class / extends |
| 实例字段 | 不能定义普通实例字段 | 可以 |
| 构造方法 | 没有 | 可以有 |
| 普通实现 | default、static、私有方法 | 普通实例方法、静态方法等 |
| 多个关系 | 一个类可实现多个接口 | 一个类只能直接继承一个类 |
| 适合场景 | 可组合能力、依赖抽象 | 共享状态、公共实现和继承层次 |
二者不是互相替代。常见设计是:抽象类保存共同状态,同时实现一个或多个接口。
15. 完整源码一:PaymentMethod.java
public interface PaymentMethod {
// 接口中的普通抽象方法默认就是 public abstract。
String getName();
boolean pay(double amount);
// default 方法带有公共实现,实现类可以直接继承或选择重写。
default boolean isValidAmount(double amount) {
return amount > 0;
}
default void printResult(double amount, boolean success) {
String status = success ? "成功" : "失败";
System.out.println(getName()
+ "支付 "
+ formatAmount(amount)
+ " 元:"
+ status);
}
// 接口静态方法属于接口本身,通过 PaymentMethod.formatAmount() 调用。
static String formatAmount(double amount) {
return String.format("%.2f", amount);
}
}
getName() 和 pay() 是协议;isValidAmount() 与 printResult() 是共享默认行为;formatAmount() 是接口级工具方法。默认方法内部调用 getName() 时仍会动态执行实现类版本。
16. 完整源码二:Refundable.java
public interface Refundable {
// 退款能力被拆成独立接口,不要求所有支付方式都必须支持。
boolean refund(double amount);
}
把退款拆成单独接口,避免强迫所有支付方式实现不支持的行为。这体现了“接口应该小而专一”的设计倾向。
17. 完整源码三:AlipayPayment.java
public class AlipayPayment implements PaymentMethod, Refundable {
// 接口不能保存每个对象独立的实例状态,余额属于实现类。
private String account;
private double balance;
public AlipayPayment(String account, double balance) {
this.account = account == null ? "未知账号" : account.strip();
this.balance = Math.max(balance, 0.0);
}
@Override
public String getName() {
return "支付宝";
}
@Override
public boolean pay(double amount) {
// 直接复用 PaymentMethod 提供的默认校验方法。
if (!isValidAmount(amount) || amount > balance) {
return false;
}
balance -= amount;
System.out.println("支付宝账号 " + account + " 完成扣款。");
return true;
}
@Override
public boolean refund(double amount) {
if (!isValidAmount(amount)) {
return false;
}
balance += amount;
return true;
}
public double getBalance() {
return balance;
}
}
一个类使用逗号同时实现两个接口。它继承支付接口的默认校验,并为三个抽象方法提供公开实现。
18. 完整源码四:WeChatPayment.java
public class WeChatPayment implements PaymentMethod {
private String userId;
private double dailyLimit;
private double paidToday;
public WeChatPayment(String userId, double dailyLimit) {
this.userId = userId == null ? "未知用户" : userId.strip();
this.dailyLimit = Math.max(dailyLimit, 0.0);
}
@Override
public String getName() {
return "微信支付";
}
@Override
public boolean pay(double amount) {
// 先验证金额,再检查本次支付是否会超过每日限额。
if (!isValidAmount(amount) || paidToday + amount > dailyLimit) {
return false;
}
paidToday += amount;
System.out.println("微信用户 " + userId + " 完成支付。");
return true;
}
public double getRemainingLimit() {
return dailyLimit - paidToday;
}
}
支付宝按余额判断,微信按每日限额判断。两者内部算法不同,但都遵守 PaymentMethod,所以对 PaymentService 来说可以互换。
19. 完整源码五:PaymentService.java
public class PaymentService {
// 依赖接口,而不是依赖 AlipayPayment 或 WeChatPayment 具体类。
private PaymentMethod paymentMethod;
public PaymentService(PaymentMethod paymentMethod) {
setPaymentMethod(paymentMethod);
}
public boolean setPaymentMethod(PaymentMethod paymentMethod) {
if (paymentMethod == null) {
return false;
}
this.paymentMethod = paymentMethod;
return true;
}
public boolean checkout(double amount) {
// 构造时可能传入 null,所以调用前仍进行防御性检查。
if (paymentMethod == null) {
System.out.println("尚未配置支付方式。");
return false;
}
// 运行时由当前接口引用指向的真实实现决定执行哪个 pay()。
boolean success = paymentMethod.pay(amount);
paymentMethod.printResult(amount, success);
return success;
}
}
PaymentService 没有导入或判断任何具体支付类。它只知道接口。这使新增实现时符合“对扩展开放、对修改相对关闭”的方向。
20. 完整源码六:PaymentDemo.java
public class PaymentDemo {
public static void main(String[] args) {
AlipayPayment alipay = new AlipayPayment("user@example.com", 1000.0);
WeChatPayment weChat = new WeChatPayment("wx-user-001", 500.0);
// PaymentService 只要求 PaymentMethod,不关心具体支付类型。
PaymentService paymentService = new PaymentService(alipay);
paymentService.checkout(200.0);
// 运行时更换策略,无需修改 PaymentService 的 checkout 逻辑。
paymentService.setPaymentMethod(weChat);
paymentService.checkout(300.0);
paymentService.checkout(250.0); // 超过剩余限额,支付失败。
// AlipayPayment 同时实现 Refundable,因此具备退款能力。
Refundable refundable = alipay;
boolean refunded = refundable.refund(50.0);
System.out.println("支付宝退款是否成功:" + refunded);
System.out.println("支付宝剩余余额:" + alipay.getBalance());
// 静态方法通过接口名调用,不能通过实现对象参与多态。
System.out.println("格式化金额:" + PaymentMethod.formatAmount(88.5));
}
}
状态变化推演
| 操作 | 结果 | 状态变化 |
|---|---|---|
| 支付宝支付 200 | 成功 | 余额 1000 → 800 |
| 切换微信支付 | 成功 | service 的接口引用改指向微信对象 |
| 微信支付 300 | 成功 | 当日已付 0 → 300,剩余额度 200 |
| 微信再付 250 | 失败 | 超过剩余额度,状态不变 |
| 支付宝退款 50 | 成功 | 余额 800 → 850 |
21. 独立练习:消息发送策略
需求:
MessageSender规定发送协议与公共校验;EmailSender验证邮箱包含@;SmsSender验证手机号长度为 11;NotificationService只依赖接口;- 运行时从邮件切换到短信。
MessageSender.java 完整答案
public interface MessageSender {
String getChannelName();
boolean send(String recipient, String message);
default boolean isValid(String recipient, String message) {
return recipient != null
&& !recipient.isBlank()
&& message != null
&& !message.isBlank();
}
}
默认校验中的 && 具有短路特性。recipient == null 时不会调用 recipient.isBlank()。
EmailSender.java 完整答案
public class EmailSender implements MessageSender {
@Override
public String getChannelName() {
return "电子邮件";
}
@Override
public boolean send(String recipient, String message) {
// 本课只模拟发送;真实项目会调用邮件服务器或第三方 API。
if (!isValid(recipient, message) || !recipient.contains("@")) {
return false;
}
System.out.println("向邮箱 " + recipient + " 发送:" + message);
return true;
}
}
SmsSender.java 完整答案
public class SmsSender implements MessageSender {
@Override
public String getChannelName() {
return "短信";
}
@Override
public boolean send(String recipient, String message) {
if (!isValid(recipient, message) || recipient.length() != 11) {
return false;
}
System.out.println("向手机号 " + recipient + " 发送:" + message);
return true;
}
}
两个实现复用相同的空值校验,再添加各自渠道规则。
NotificationService.java 完整答案
public class NotificationService {
private MessageSender sender;
public NotificationService(MessageSender sender) {
this.sender = sender;
}
public void setSender(MessageSender sender) {
if (sender != null) {
this.sender = sender;
}
}
public boolean notifyUser(String recipient, String message) {
if (sender == null) {
System.out.println("未配置消息发送器。");
return false;
}
boolean success = sender.send(recipient, message);
System.out.println(sender.getChannelName()
+ "发送结果:"
+ (success ? "成功" : "失败"));
return success;
}
}
NotificationDemo.java 完整答案
public class NotificationDemo {
public static void main(String[] args) {
NotificationService service = new NotificationService(new EmailSender());
// 首先使用邮件实现。
service.notifyUser("learner@example.com", "Java 第十课已更新");
// 切换为短信实现,NotificationService 本身不用修改。
service.setSender(new SmsSender());
service.notifyUser("13812345678", "请继续完成接口练习");
// 无效手机号会被 SmsSender 自己的规则拒绝。
service.notifyUser("123", "这条消息不会成功发送");
}
}
独立练习的关键不是邮件或短信本身,而是 NotificationService 从未出现:
if (类型是 EmailSender) { ... }
else if (类型是 SmsSender) { ... }
不同规则被封装在实现类中,业务服务只调用统一接口。
22. 常见错误
实现方法没有 public
接口方法隐含 public,实现时不能降低权限。
试图 new 接口
接口不完整,必须创建实现类对象。
把对象状态写进接口常量
接口字段是共享常量,余额、账号等实例状态应放在实现类。
通过对象调用接口静态方法
静态方法通过接口名调用,不参与实例多态。
一个大接口塞入所有能力
如果某些实现不支持退款,不应强迫它们实现假的退款方法。拆成 Refundable 小接口。
业务类写死具体实现
字段和构造参数优先声明接口类型,让实现可以替换。
default 方法冲突未解决
两个接口提供相同默认方法时,实现类必须明确重写。
把策略模式理解成大量 if/else
策略模式的核心恰恰是用多态替代根据类型分支。
23. 检查题与答案
问题
- 接口普通抽象方法隐含哪两个修饰符?
- 实现接口使用什么关键字?
- 接口能否直接
new? - 接口能否拥有构造方法和普通实例字段?
- 接口字段默认是什么性质?
default方法有什么作用?- 接口静态方法应该怎样调用?
- 一个类能否同时实现多个接口?
- 一个类能否同时继承父类并实现接口?语法顺序是什么?
- 接口能否继承多个接口?
- 两个接口的默认方法冲突时怎么办?
PaymentMethod method = new AlipayPayment(...)的编译时类型和运行时类型是什么?- 为什么
PaymentService字段声明为PaymentMethod? - 策略模式中的策略接口、具体策略和上下文分别是什么?
- 接口与抽象类在实例状态方面有什么区别?
- 为什么退款能力单独拆成
Refundable? recipient != null && !recipient.isBlank()如何利用短路?- 新增银行卡支付时,理想情况下是否需要修改
PaymentService.checkout()? - 实现接口方法时能否省略
public? - 为什么大量
instanceof和类型分支可能意味着设计需要改进?
答案
public abstract。implements。- 不能,只能创建实现类对象并由接口引用保存。
- 不能;对象状态和构造初始化属于实现类或抽象类。
- 自动是
public static final常量。 - 为实现类提供可继承或重写的公共实例实现,也便于接口演进。
- 使用接口名,例如
PaymentMethod.formatAmount()。 - 可以,接口名之间用逗号分隔。
- 可以;先
extends Parent,再implements First, Second。 - 可以,接口使用
extends继承多个接口。 - 实现类必须重写冲突方法,并可用
First.super.method()明确选择默认实现。 - 编译时类型是
PaymentMethod,运行时类型是AlipayPayment。 - 让服务依赖支付协议,从而可替换不同实现,降低耦合。
PaymentMethod是策略接口;支付宝和微信是具体策略;PaymentService是上下文。- 抽象类可以保存实例字段,接口不能保存每个对象独立的实例状态。
- 不是所有支付方式都支持退款,小接口避免强迫无关实现承担不适用的方法。
- 左侧为
false时右侧不执行,从而避免对null调用方法。 - 不需要。新增类实现
PaymentMethod后即可传入。 - 不能,接口要求公开方法,实现不能缩小访问权限。
- 共同变化行为可能没有被抽象进接口或多态方法,导致调用者了解过多具体类型。
24. 本课完成标准
- 能定义接口并创建两个实现类。
- 能解释接口抽象方法、默认方法和静态方法的区别。
- 能让一个类实现多个小接口。
- 能使用接口引用和接口参数完成多态调用。
- 能解释接口与抽象类的主要差异。
- 能让业务类依赖接口并在运行时切换策略。
- 能说出支付案例中的策略模式角色。
- 能解释这种设计与未来 Spring 依赖注入的关系。
下一课学习异常处理:异常体系、try-catch-finally、throw、throws、自定义异常,以及 try-with-resources 自动关闭资源。