第 10 课:接口、依赖抽象与策略模式入门

2 阅读14分钟

第 10 课:接口、依赖抽象与策略模式入门

本课目标

学完后,你应该能够:

  1. 使用 interface 声明行为协议。
  2. 使用 implements 实现接口,并正确重写接口方法。
  3. 理解接口抽象方法隐含的 public abstract
  4. 使用接口的 default 方法和 static 方法。
  5. 让一个类同时实现多个接口。
  6. 使用接口引用、接口数组和接口参数实现多态。
  7. 理解接口不能保存对象实例状态,也没有构造方法。
  8. 区分接口与抽象类的职责。
  9. 让业务类依赖抽象,而不是写死具体实现。
  10. 理解策略模式与依赖注入的基本思想。

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 / implementsabstract class / extends
实例字段不能定义普通实例字段可以
构造方法没有可以有
普通实现defaultstatic、私有方法普通实例方法、静态方法等
多个关系一个类可实现多个接口一个类只能直接继承一个类
适合场景可组合能力、依赖抽象共享状态、公共实现和继承层次

二者不是互相替代。常见设计是:抽象类保存共同状态,同时实现一个或多个接口。

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. 检查题与答案

问题

  1. 接口普通抽象方法隐含哪两个修饰符?
  2. 实现接口使用什么关键字?
  3. 接口能否直接 new
  4. 接口能否拥有构造方法和普通实例字段?
  5. 接口字段默认是什么性质?
  6. default 方法有什么作用?
  7. 接口静态方法应该怎样调用?
  8. 一个类能否同时实现多个接口?
  9. 一个类能否同时继承父类并实现接口?语法顺序是什么?
  10. 接口能否继承多个接口?
  11. 两个接口的默认方法冲突时怎么办?
  12. PaymentMethod method = new AlipayPayment(...) 的编译时类型和运行时类型是什么?
  13. 为什么 PaymentService 字段声明为 PaymentMethod
  14. 策略模式中的策略接口、具体策略和上下文分别是什么?
  15. 接口与抽象类在实例状态方面有什么区别?
  16. 为什么退款能力单独拆成 Refundable
  17. recipient != null && !recipient.isBlank() 如何利用短路?
  18. 新增银行卡支付时,理想情况下是否需要修改 PaymentService.checkout()
  19. 实现接口方法时能否省略 public
  20. 为什么大量 instanceof 和类型分支可能意味着设计需要改进?

答案

  1. public abstract
  2. implements
  3. 不能,只能创建实现类对象并由接口引用保存。
  4. 不能;对象状态和构造初始化属于实现类或抽象类。
  5. 自动是 public static final 常量。
  6. 为实现类提供可继承或重写的公共实例实现,也便于接口演进。
  7. 使用接口名,例如 PaymentMethod.formatAmount()
  8. 可以,接口名之间用逗号分隔。
  9. 可以;先 extends Parent,再 implements First, Second
  10. 可以,接口使用 extends 继承多个接口。
  11. 实现类必须重写冲突方法,并可用 First.super.method() 明确选择默认实现。
  12. 编译时类型是 PaymentMethod,运行时类型是 AlipayPayment
  13. 让服务依赖支付协议,从而可替换不同实现,降低耦合。
  14. PaymentMethod 是策略接口;支付宝和微信是具体策略;PaymentService 是上下文。
  15. 抽象类可以保存实例字段,接口不能保存每个对象独立的实例状态。
  16. 不是所有支付方式都支持退款,小接口避免强迫无关实现承担不适用的方法。
  17. 左侧为 false 时右侧不执行,从而避免对 null 调用方法。
  18. 不需要。新增类实现 PaymentMethod 后即可传入。
  19. 不能,接口要求公开方法,实现不能缩小访问权限。
  20. 共同变化行为可能没有被抽象进接口或多态方法,导致调用者了解过多具体类型。

24. 本课完成标准

  • 能定义接口并创建两个实现类。
  • 能解释接口抽象方法、默认方法和静态方法的区别。
  • 能让一个类实现多个小接口。
  • 能使用接口引用和接口参数完成多态调用。
  • 能解释接口与抽象类的主要差异。
  • 能让业务类依赖接口并在运行时切换策略。
  • 能说出支付案例中的策略模式角色。
  • 能解释这种设计与未来 Spring 依赖注入的关系。

下一课学习异常处理:异常体系、try-catch-finallythrowthrows、自定义异常,以及 try-with-resources 自动关闭资源。